1.2.34 · D2Introduction to Programming (Python)

Visual walkthrough — List comprehensions — `[expr for x in iterable if condition]`

2,331 words11 min readBack to topic

Step 1 — The conveyor belt (what an "iterable" even is)

WHAT. Before any code, picture a belt carrying items past you, one at a time. In Python that belt is called an iterable: anything a for loop can walk through — a list, a range, a string. (More on what qualifies: range() and iterables.)

WHY. Every comprehension begins with the phrase for x in iterable. If you don't picture where the items come from, the rest is symbol-pushing. So we anchor the very first symbol, iterable, to a moving belt.

PICTURE. Below, the belt carries the numbers 0,1,2,3,4 (this is range(5)). The little pointer labelled x sits on whichever item is currently under inspection. x is not a fixed number — it is the name of "the current item", and it slides forward one notch at a time.

Figure — List comprehensions — `[expr for x in iterable if condition]`

Step 2 — The honest 4-line loop (our ground truth)

WHAT. We write the plain loop that builds "squares of even numbers". Nothing clever — the boring version everyone can read.

result = []              # (a) empty box
for x in range(5):       # (b) belt: x = 0,1,2,3,4
    if x % 2 == 0:       # (c) guard: keep only evens
        result.append(x**2)   # (d) machine: square it, drop in box

WHY. This is our ground truth. Every later one-liner must produce exactly what this produces, or it is wrong. We will never memorise the comprehension — we will always be able to re-derive it from this.

PICTURE. Four stations in a row: the empty box (a), the belt (b), the guard (c) that throws items away, and the machine (d) that transforms survivors and drops them in the box. Follow the arrows: an item only reaches the box if it survives the guard.

Figure — List comprehensions — `[expr for x in iterable if condition]`

Term-by-term, reading the annotated code:

  • result = [] — the box that starts empty and grows.
  • for x in range(5) — the belt; x takes each value.
  • if x % 2 == 0 — the guard; x % 2 is the remainder after dividing by 2, so == 0 means "even".
  • x**2 — the machine; ** is Python's "to the power of", so x**2 is .
  • .append(...) — "put this in the box, at the end".

Step 3 — Trace one full pass (watch the box fill)

WHAT. Run the loop by hand, tick by tick, and record what happens at the guard and the box.

WHY. The single most common misreading (per the parent's mistake callout) is thinking expr runs first. Tracing kills that idea: you literally cannot square an item before the belt has handed it to you and the guard has let it through. The true order is belt → guard → machine.

PICTURE. A table-as-picture: five rows, one per tick. Green rows survive the guard; faded rows are thrown away; the rightmost column shows the box growing.

Figure — List comprehensions — `[expr for x in iterable if condition]`
tick x x % 2 == 0? x**2 box after
1 0 ✅ True 0 [0]
2 1 ❌ False [0]
3 2 ✅ True 4 [0, 4]
4 3 ❌ False [0, 4]
5 4 ✅ True 16 [0, 4, 16]

Final box: [0, 4, 16]. Keep this number in mind — every one-liner below must reproduce it.


Step 4 — Name the three slots

WHAT. Pull the three interchangeable pieces out of the loop and give them their official slot names.

WHY. The comprehension is nothing but these three slots, rearranged. Once you can point at each one in the loop, the one-liner is just a re-shuffle — no new logic to learn.

PICTURE. The loop with three coloured boxes drawn around exactly the parts that will move: the machine x**2 (coral), the belt for x in range(5) (lavender), the guard if x % 2 == 0 (mint).

Figure — List comprehensions — `[expr for x in iterable if condition]`

Step 5 — The fold (loop → one line)

WHAT. Physically move the three coloured blocks onto one line, inside square brackets [ ].

WHY. The empty box and the .append are pure mechanics — Python can supply them for free. What actually carries meaning is only the three slots. So we drop the mechanics and keep the meaning, arranged in reading order: expr first (it's the answer we care about), then the belt, then the guard.

PICTURE. An animation-in-one-frame: dashed arrows carry each coloured block up out of the loop and into the bracketed line. The box and append fade away with a strikethrough.

Figure — List comprehensions — `[expr for x in iterable if condition]`

Reading each symbol where it sits:

  • [ ... ] — the brackets are the box; Python creates it and appends for us.
  • x**2 — same machine as line (d); it just moved to the front because we read the answer first.
  • for x in range(5) — same belt as line (b), unchanged.
  • if x % 2 == 0 — same guard as line (c), now written after the belt.
[x**2 for x in range(5) if x % 2 == 0]   # → [0, 4, 16]  ✓ matches Step 3

Step 6 — Reading order vs execution order (the trap, drawn)

WHAT. Show the two orders on top of each other so the mismatch is impossible to miss.

WHY. Your eye reads x**2 first, so your brain assumes it runs first. It does not. Python still runs belt → guard → machine (Step 3 proved it). The one-liner only rearranges the writing, never the execution.

PICTURE. Two arrows over the same line: a top arrow (reading) pointing left→right starting at expr; a bottom arrow (execution) that jumps to the for first, then if, then loops back to expr.

Figure — List comprehensions — `[expr for x in iterable if condition]`

Step 7 — The two ifs live on opposite sides (edge case)

WHAT. There are two completely different ifs, and they sit on opposite sides of the for. Nail down which is which.

WHY. This is the #1 real bug. A filter if drops items and has no else; it goes after for. A ternary if/else chooses a value and always needs an else; it lives inside expr, so it goes before for. (Deep dive on the value-chooser: Conditional expressions (ternary).)

PICTURE. The for sits in the middle like a fence. On the left (before for) stands the value-chooser n if n>0 else 0. On the right (after for) stands the item-dropper if n>0. Two little belts show the different outputs.

Figure — List comprehensions — `[expr for x in iterable if condition]`
nums = [3, -1, 4, -5]
 
# FILTER if (after for): drops items → shorter list
[n for n in nums if n > 0]        # → [3, 4]
 
# TERNARY if/else (before for, inside expr): keeps all, changes value
[n if n > 0 else 0 for n in nums] # → [3, 0, 4, 0]
  • Filter output has fewer items (the -1, -5 are gone).
  • Ternary output has the same length (negatives became 0, nothing dropped).

Step 8 — Degenerate & boundary cases (never get surprised)

WHAT. Walk every corner: empty belt, guard that rejects everything, guard that accepts everything, and no guard at all.

WHY. The contract says the reader must never hit a scenario we didn't show. Comprehensions behave gracefully at every edge — they simply return whatever the loop would have.

PICTURE. Four mini-belts side by side, each with its resulting box drawn underneath.

Figure — List comprehensions — `[expr for x in iterable if condition]`
case code result why
empty belt [x**2 for x in []] [] belt never moves → box stays empty
guard rejects all [x for x in range(5) if x > 99] [] every item fails the guard
guard accepts all [x for x in range(3) if x >= 0] [0, 1, 2] guard is always True → same as no if
no guard [x*10 for x in range(3)] [0, 10, 20] if is optional; every item kept

An empty result is not an error — it is the honest answer "nothing survived". Same as the loop building an empty box.

Recall Check yourself

What does [x for x in [] if x > 0] return? ::: [] — the belt is empty, so the loop body never runs. What does [x for x in range(4) if True] return? ::: [0, 1, 2, 3] — a guard that is always True keeps everything.


The one-picture summary

Everything above, compressed: the 4-line loop folds into one line; the three slots keep their meaning and only change position; execution still flows belt → guard → machine even though we read machine-first; and the two ifs straddle the for.

Figure — List comprehensions — `[expr for x in iterable if condition]`
Recall Feynman retelling of the whole walkthrough

We started with a conveyor belt of items and a factory in four stations: an empty box, the belt, a guard, and a juicing machine. That's the honest loop. We traced it by hand and got [0, 4, 16], and while tracing we noticed the belt always moves first, the guard checks second, and the machine runs last — no matter how we later write it.

Then we realised the empty box and the "put in box" step are just chores Python can do for us. The only parts that carry meaning are three slots: the machine (expr), the belt (for x in ...), and the guard (if ...). So we lifted those three blocks onto one line inside [ ], putting the machine at the front because humans like the answer first. Same list, less typing.

One catch: writing the machine first tricks the eye into thinking it runs first — it doesn't. And there are two guards that look alike but aren't: the dropping guard (if, no else) goes after the belt; the choosing guard (if/else) lives inside the machine and goes before the belt. Choose-before, cut-after. Finally, at every strange edge — empty belt, nothing passes, everything passes — the factory just calmly hands back whatever the loop would have built, sometimes an empty box, and that's perfectly fine.


Connections

  • For loops — the loop this whole page folds up from.
  • Conditional expressions (ternary) — the "choose a value" if/else from Step 7.
  • range() and iterables — what feeds the belt in for x in iterable.
  • map() and filter()map is the machine, filter is the guard, as standalone functions.
  • Generator expressions — same fold, but with ( ), building items lazily instead of all at once.
  • Dictionary and set comprehensions — the identical fold with { }.

Concept Map

supplies x

keeps x

drops value in

folds into

drop a value

drop an item

belt of items iterable

guard if condition

machine expr

new list

honest 4 line loop

one line comprehension

if else before for

if after for