List & Dict Comprehensions
Theme
Palette
Download
Caption (tap to copy)
📖 Deep dive (full written explanation)
This cover opens the mechanics post by promising to demystify the parts of comprehensions that feel like magic: nesting order and scoping. The framing — that the quirks stop being mysterious once you watch Python evaluate one step by step — sets the tone for a precise, machine-level explanation rather than hand-waving.
The magnifying-glass framing signals that this is the engine-room post. Where the concept post gave the shape and the why post gave the motivation, this one gives the exact rules that let you predict any comprehension's behavior without running it.
The reading-order rule is the single most useful mechanic to internalize. The output expression is written first but evaluated last. To read a comprehension correctly, mentally move the output to the end: 'for each x in source, if the condition holds, compute the expression and collect it.' That reordering matches the actual execution.
This inversion is exactly why comprehensions confuse newcomers — the syntax leads with the result, which is the opposite of execution order. Once you've trained yourself to read the for-clause first and the output last, even unfamiliar comprehensions parse cleanly. Every other rule in this post builds on getting this order straight.
The pipeline diagram makes the true evaluation order explicit as a sequence of stages: take the next item from the source, test the filter to keep or skip it, compute the output expression, and collect the result. This is the runtime reality behind the syntax's reversed appearance.
Laying it out as a pipeline reinforces that each element flows through these stages independently. There's no shared state carried between elements — a fact that matters enormously for the accumulation limitation covered in the code post and the closure trap in the mistakes post. Each item is processed in isolation, then collected.
Nested for-clauses are where ordering bugs cluster, so the rule deserves emphasis: multiple fors read left to right exactly as you'd nest the equivalent loops, with the leftmost for as the outermost loop. '[x for row in grid for x in row]' is 'for each row, then for each x in that row.'
The mental aid that prevents nearly all nesting mistakes is to imagine indenting the fors as loops. Write them in the order you'd type the nested 'for' statements, outer first. If you can picture the indentation, you can order the comprehension. This directly defuses the misordering mistake flagged at the end of the post.
This code slide proves the nesting rule by showing the comprehension and its expanded loop side by side. Flattening a grid — 'for row in grid, for x in row' — produces the same result whether written as one comprehension line or two nested loops, and the for-clauses appear in the identical order in both.
The comments mark which for is outer and which is inner, mapping the comprehension's left-to-right clauses onto the loop's outer-to-inner indentation. Running this and then deliberately swapping the two for-clauses (which raises a NameError because 'row' isn't defined yet) is an instructive way to feel the rule from the inside.
Scoping is a frequently-misunderstood mechanic, and Python 3 made it clean: a comprehension runs inside its own implicit function, so the loop variable is local to that hidden scope and never leaks into the surrounding code. After '[n for n in range(5)]', the name 'n' does not exist outside.
This is a deliberate correction of a Python 2 wart, where the loop variable did leak and could clobber an outer variable of the same name. Knowing the variable is scoped lets you reuse short loop names like x, n, or i inside comprehensions freely, without worrying about collisions with names in the enclosing function.
This code slide demonstrates the scoping rule concretely by building a list, then attempting to print the loop variable afterward and catching the resulting NameError. Seeing the exception confirms that the name genuinely doesn't survive the comprehension.
The practical takeaway is reassurance: you can pick the most natural loop-variable name without polluting the surrounding namespace or accidentally overwriting an existing variable. This safety is part of why comprehensions compose so cleanly and why you can scatter many of them through a function without bookkeeping about variable names.
This slide draws the crucial distinction between the two jobs the 'if' keyword can do, governed entirely by position. An 'if' placed after the for-clause, with no else, is a filter — it decides inclusion. An 'if/else' placed before the for is a conditional (ternary) expression — it changes each value but keeps every element.
Confusing these silently changes the length of your result, which is exactly why it's a recurring bug. A filter shortens the output; a ternary preserves its length. Holding the rule 'if at the end filters, if/else up front transforms' prevents the data-loss-or-duplication errors that the mistakes post examines in detail.
The code makes the if-versus-ternary distinction unmistakable by showing both on the same source. The filter form keeps only the odd numbers, producing a shorter list. The ternary form labels every number, producing a same-length list of strings. Same keyword, opposite effect on length.
Notice the structural giveaway: the filter has no else and sits after 'for'; the ternary has an else and sits before 'for'. Training your eye to spot 'is there an else, and where does the if sit relative to for' lets you classify any comprehension's intent instantly, which is the first defense against the silent length bugs covered later.
Generator expressions are introduced here as the lazy sibling that shares all the same evaluation rules but produces items one at a time instead of materializing the whole collection. Swapping square brackets for parentheses is the only syntactic change; the semantics of how items are computed are identical.
The distinction matters for memory and is the foundation of the 'building a list you'll only stream' mistake in the next post. When you iterate the result exactly once — feeding sum(), any(), or a for-loop — a generator uses near-constant memory regardless of source size. The same evaluation model, lazily applied, is the whole idea.
The tips slide consolidates the post's mechanics into five rules you can recall on demand: output is evaluated last, the leftmost for is the outermost loop, the loop variable is scoped and never leaks, 'if' at the end filters while 'if/else' up front maps, and parentheses make it a lazy generator.
Together these rules let you predict any comprehension's behavior — its result, its length, and its memory profile — by inspection alone. That predictive power is the real deliverable of the post: not just reading comprehensions, but reasoning about them with confidence before you ever run the code.
The closing mistake targets the most common mechanical error: misordering nested for-clauses, which produces either a NameError (referencing a variable before its loop defines it) or quietly wrong results. The fix is the mechanical rule from earlier — write the fors in the exact order you'd nest the loops, outer first.
Framing it as 'if you can indent it as a loop, you can order the comprehension' gives a concrete, repeatable check. When a nested comprehension misbehaves, expanding it into explicit loops to verify the order is always the fastest debugging path, and it reinforces the equivalence the concept post established.
This CTA moves from theory to practice. Having built the precise mental model of how comprehensions evaluate, the reader is ready to drill the concrete patterns that turn that understanding into fluency.
The teaser promises a hands-on, code-heavy tour of the patterns you'll actually type week to week — mapping, filtering, building dicts, deduping, flattening — which is exactly what the Code Example post delivers.