List & Dict Comprehensions
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This cover frames the final angle as a field guide to failure modes. The hook makes an important point: the very qualities that make comprehensions elegant — their compactness and expressiveness — are also what make them dangerous when misused. Sharp tools cut both ways.
The emphasis on 'most comprehension bugs aren't syntax errors' is deliberate. Beginners expect mistakes to announce themselves with a traceback, but the worst comprehension problems are silent: wrong-length results, memory blowups, and unreadable code that passes review. Learning to anticipate these is what separates safe production code from clever-looking traps.
The first and most common mistake is the unreadable mega-comprehension. Stacking multiple for-clauses, filters, and a ternary into one line produces something that's technically valid but slower to read than the loop it replaced and nearly impossible to debug step by step.
The practical heuristic is simple: if you can't grasp the comprehension in a single glance, that's the signal to unfold it into a loop. This isn't a failure — it's good judgment. The whole value of a comprehension is clarity, and once a line crosses the complexity threshold, the loop is the clearer choice. Recognizing that threshold is a skill worth developing deliberately.
This code slide demonstrates the readability cliff with a concrete contrast. The dense version packs two for-clauses, two filters, and a function call into one line that demands real effort to parse. The unfolded loop expresses the identical logic in a form you can read top to bottom and set a breakpoint inside.
The debuggability point is underrated. You can't put a breakpoint partway through a comprehension or inspect intermediate values — it's all or nothing. When logic is complex enough that you'll need to debug it, the explicit loop isn't just more readable, it's the only form you can actually step through. That alone justifies unfolding dense comprehensions.
The second mistake is a performance and memory trap: building a full list when you only intend to stream the result once. A list comprehension materializes every element in memory simultaneously. If you immediately feed that list to sum(), any(), or a single for-loop, you've allocated a potentially enormous throwaway structure for no reason.
The fix is the generator expression — parentheses instead of brackets — which yields items one at a time and uses near-constant memory regardless of source size. This connects directly to the laziness covered in the mechanics post. The habit to build: whenever a comprehension's result is consumed exactly once, reach for () instead of [].
This code slide quantifies the memory mistake. Summing ten million squares via a list comprehension first allocates a ten-million-element list, then sums it. The generator-expression version produces each square on demand and discards it after adding to the running total, so peak memory stays tiny.
The answers are identical; only the resource profile differs. On small inputs the difference is invisible, which is exactly why the mistake survives into production — it works fine in testing and only bites when the data grows. Defaulting to generators for single-pass consumption is cheap insurance against that scaling surprise.
The third mistake is the filter-versus-ternary mix-up, revisited here as a failure mode. The danger is that confusing the two silently changes your result's length. A trailing 'if' filters and shortens the output; an 'if/else' before the for transforms and preserves length. There's no error — just data that's quietly wrong.
Because it fails silently, this bug is especially insidious. You might drop rows you meant to keep or keep rows you meant to relabel, and nothing complains. The defense is the positional rule drilled in the mechanics post: check whether the 'if' has an else and where it sits relative to 'for' before trusting the comprehension's output.
This code slide shows the two forms producing visibly different results from the same source. The filter version returns just the odd numbers, a shorter list. The ternary version returns a same-length list where evens became zero. Putting them side by side makes the length difference impossible to miss.
The structural tells are worth memorizing: the filter has no else and trails the for-clause; the ternary has an else and precedes it. When reviewing or writing a comprehension, locating the 'if' and checking for an 'else' is a two-second check that catches this entire class of silent length bugs before they ship.
The fourth mistake is using a comprehension for side effects rather than for building a collection. The canonical abuse — '[print(x) for x in items]' — runs the prints but also constructs a list of None values that's immediately discarded. Calling print(), writing files, or appending to an external list inside a comprehension obscures intent and wastes the allocation.
The principle is clean: comprehensions are for collecting a result. If you're not collecting anything — if the point is to perform an action per item — a plain for-loop is both clearer and correct, and it wastes nothing. The loop's explicit form signals 'do this for each item' precisely. Beyond the wasted list, the deeper problem is communication: a reader seeing a comprehension expects a value worth keeping. When the real purpose is a side effect, the comprehension lies about intent and the loop tells the truth. This is a case where the 'declarative is better' rule from the why post inverts — when there's no result to declare, the imperative loop is the honest choice.
The fifth mistake is the subtle late-binding closure trap, which catches even experienced developers. Building a list of lambdas in a comprehension captures the loop variable by reference, not by value — so when the lambdas are eventually called, they all see the variable's final value rather than the value at creation time.
This surprises people because the lambdas look like they should each remember their own 'i'. They don't; Python closures capture variables, not values. The standard fix is binding the current value as a default argument ('lambda i=i'), which snapshots it at definition time. This bites hardest when building lists of callbacks, event handlers, or deferred computations in a loop.
This code slide makes the closure trap and its fix concrete. The buggy version produces three functions that all return 2 — the last value of i — because they share the same captured variable. The fixed version, using 'lambda i=i', binds each function's i to the current value at creation, yielding the expected 0, 1, 2.
The 'i=i' idiom looks odd but is precise: the right-hand i is evaluated immediately at lambda-creation time and stored as the default for the left-hand parameter i. It's the standard Python remedy for late binding in any loop, not just comprehensions, and worth recognizing on sight since the bug it fixes is otherwise baffling to diagnose.
The decision diagram distills the day's central judgment call into a flowchart. First: are you collecting a new collection at all? If not, the answer is a plain loop because the work is side effects. If yes, the follow-up is whether it fits readably on one line — if so, use a comprehension; if not, unfold to a loop.
This single chart captures the two most important rules from the entire day: comprehensions are for building collections, and only when the result stays glanceable. Running any candidate through these two questions reliably steers you to the right construct, defusing the readability and side-effect mistakes in one decision.
The tips slide consolidates all five mistakes into a defensive checklist: keep it glanceable or unfold it; use a generator when streaming once; remember 'if' at the end filters while 'if/else' up front maps; never put side effects inside; and bind loop variables with 'i=i' in lambdas. Each bullet maps to one failure mode from the post.
Designed as a final reference, this card lets a reader audit any comprehension they write against the known traps. Together with the patterns card from the code post, it forms a complete practical toolkit — the patterns tell you what to write, and these tips tell you what to avoid.
This CTA closes both the mistakes post and the full five-post arc on comprehensions. The reader now has the concept, the motivation, the mechanics, the patterns, and the pitfalls — a complete picture of when and how to use list and dict comprehensions well.
The teaser points forward to the next entry in the series, keeping the momentum of the 100 Days of AI sequence while signaling that comprehensions are now a tool the reader can deploy confidently as the Python foundation continues to build.