How to Print-Debug Python Comprehensions
Comprehensions are a fantastic language feature in Python. They are an elegant alternative to manually constructing and populating data structures. Comprehensions are declarative – they just say what they are, as opposed to the implicit logic of manual looping. When it comes to simple object creation, comprehension should be used whenever possible. This goes not just for lists, but also for dictionaries and sets.
However, a widely perceived drawback to comprehensions is that they are harder to debug. When something goes wrong with a manual loop, the first thing to do is to print out the iterated values as they turn up. But the values of a list comprehension can’t be accessed, so print-debugging isn’t possible. To deal with this, it’s common to unravel the comprehension into a manual loop. Manual loops are uglier and more complicated and more error-prone than comprehensions, but that’s the price that must be paid for debuggability.
That’s the perception at least, but it’s wrong. In fact, print-debugging comprehensions is easy. The key fact to understand is that print
is a function, and it can occur anywhere that a function can occur. In particular, print
can occur in a comprehension filter.
As an example, here’s some code that deals with graphs:
Notice the nested comprehensions: the dictionary comprehension contains set comprehensions as its values. Unraveling this into a manual loop would be just awful, but perhaps necessary to print the values as they show up:
(As a side note, statements like ret = {}
are a code smell and often an indication that a comprehension could be used instead.)
Rather than go through the hassle of unraveling the comprehensions, we can simply print the values as part of the comprehension filter. The print
function always returns None
, so it’s just a matter of creating a vacuously true filter that touches every iterated value but doesn’t discard any of them:
It isn’t pretty. But then again, neither is print-debugging.
This technique can be used in other places where debugging might be considered difficult, like in a chain of boolean checks:
It might happen that all the items in the sequence are failing the test conditions, and so none of them make it to do_stuff
. To see where they are being caught, print
calls can be added between the conditions:
(Note that this example uses an or
-chain, and so the dummy print
conditions need to be vacuously false rather than true.)
Again, this technique is possible because print
is a function. In older versions of Python, print
was a statement. That was a bad idea, and fortunately it was rectified. In general, statements are clunkier and less flexible than values. Python continues to improve with the addition of value-oriented language features like the walrus operator.