This is the "throw everything at it" companion to [[1.2.34 List comprehensions — `[expr for x in iterable if condition]`|the parent note]]. There, we learned the shape [expr for x in iterable if condition]. Here, we run it through every kind of situation a comprehension can meet, so that no future problem is a surprise.
We assume nothing beyond the parent. If a word like "iterable" or "ternary" appears, we re-anchor it the moment we use it.
Before working examples, let us map the whole territory . A list comprehension has three slots — expr, for, if — and each slot can be in one of a few "states". The table below lists every distinct case class this topic can throw at you. Every later example is tagged with the cell it covers.
#
Case class
What is special about it
Covered by
A
Transform only (no filter)
Every item survives; only expr acts
Example 1
B
Filter only (expr = item)
Nothing changes; items just get kept/dropped
Example 2
C
Transform + filter together
Both slots active
Example 3
D
Ternary in expr (a if c else b)
Choosing a value , if/else goes BEFORE for
Example 4
E
Ternary AND filter in one line
Both ifs present, in different places
Example 5
F
Nested for (flatten / grid)
Two loops, outer-to-inner order
Example 6
G
Degenerate input : empty iterable, or filter kills all
Result is [] — the zero case
Example 7
H
Real-world word problem
Translate English → comprehension
Example 8
I
Exam twist : nested for + filter that depends on both loop variables
Order of evaluation must be reasoned exactly
Example 9
J
Chained filters (multiple ifs after one for)
Two ifs in a row act as AND
Example 10
We will now hit every cell A through J.
Intuition Why enumerate cases at all?
A comprehension looks tiny, so people assume "if I learned one, I learned them all." Not true: the empty result , the ==two different ifs, and the nested for ordering== each trip people who only ever saw the happy path. Seeing all ten cells once means never meeting a new shape again.
Worked example Squares of the first five whole numbers
Build the list of squares 0 2 , 1 2 , 2 2 , 3 2 , 4 2 .
Forecast: guess the output list before reading on. (What is 4 2 ?)
Step 1 — pick the iterable. We need the numbers 0 , 1 , 2 , 3 , 4 . In Python range(5) produces exactly these (an iterable = a thing you can loop over; see range() and iterables ).
Why this step? The for x in iterable slot must be filled first — it is what execution actually starts with.
Step 2 — decide the filter. We want every number, none dropped.
Why this step? Case A means "no if" — so we simply omit the filter slot.
Step 3 — decide expr. We want the square, so expr = x**2 (in Python ** means "to the power of").
Why this step? expr is what actually lands in the new list, computed last per item.
Step 4 — pour into the template.
squares = [x ** 2 for x in range ( 5 )]
# → [0, 1, 4, 9, 16]
Why this step? The three chosen slots (expr, for, no if) now sit in their fixed positions in the template [expr for x in iterable]; assembling is the mechanical last move that turns our plan into runnable code.
Verify: count = 5 items in, 5 items out (transform-only never changes length). Check the last one: 4 2 = 16 . ✓ The list is [ 0 , 1 , 4 , 9 , 16 ] .
Worked example Keep only the positive numbers
From [3, -1, 4, -5, 9, 0], keep the positives.
Forecast: which numbers survive? Is 0 positive?
Step 1 — iterable. The list itself: for n in [3, -1, 4, -5, 9, 0].
Why this step? The source of items.
Step 2 — filter. "Positive" means strictly greater than zero: if n > 0. Note 0 > 0 is False, so zero is dropped .
Why this step? The zero boundary is exactly where filters trip people — pin it down now.
Step 3 — expr. We keep items unchanged , so expr = n (just the item itself).
Why this step? Case B: the output expression is the plain loop variable, so the comprehension acts as a pure sieve.
Step 4 — assemble.
positives = [n for n in [ 3 , - 1 , 4 , - 5 , 9 , 0 ] if n > 0 ]
# → [3, 4, 9]
Why this step? We drop the three slots (n, for, if n > 0) into the full template [expr for x in iterable if condition]; assembling proves the plan is complete and runnable.
Verify: 6 in, 3 out (filters shrink or keep length, never grow it). The dropped ones are − 1 , − 5 , 0 — all failing n > 0. ✓
Worked example Squares of the even numbers 0–9
Square each number 0..9, but only the even ones.
Forecast: how many items survive the "even" filter out of ten?
Step 1 — iterable. range(10) gives 0 , 1 , … , 9 .
Why this step? The problem says "each number 0..9", and range(10) produces exactly 0 through 9 — this is the source the for slot draws from before anything else runs.
Step 2 — filter. "Even" means divisible by 2. The operator % gives the remainder of a division, so x % 2 is 0 when x is even. Thus if x % 2 == 0.
Why this step? % (modulo) is the standard tool for "is this divisible?" — it answers exactly the question "what's left over?", and leftover 0 means clean division.
Step 3 — expr. x**2.
Why this step? The problem asks for the square of each survivor, so the output expression that lands in the new list must be x**2 — the transform runs last, only on items that passed the filter.
Step 4 — assemble.
result = [x ** 2 for x in range ( 10 ) if x % 2 == 0 ]
# → [0, 4, 16, 36, 64]
Why this step? All three slots are now decided, so we place them into [expr for x in iterable if condition] — assembling is the final mechanical step that fuses transform and filter into one runnable line.
Verify: even numbers in 0..9 are 0 , 2 , 4 , 6 , 8 (five of them), squares 0 , 4 , 16 , 36 , 64 . Five in the output. ✓
Worked example Replace negatives with zero (keep length)
From [3, -1, 4, -5], turn each negative into 0 but keep every slot .
Forecast: how long is the answer — 2 or 4?
Step 1 — recognise: this is NOT a filter. We are not dropping items; we are choosing a value per item. That means a conditional expression (a ternary : see Conditional expressions (ternary) ).
Why this step? Filter vs value-choice is the single biggest fork in comprehension-land. Dropping → filter if after for. Choosing → ternary before for.
Step 2 — write the ternary. The form is value_if_true if condition else value_if_false. Here: n if n > 0 else 0. Read it as "give n when n > 0, otherwise give 0".
Why this step? A ternary always returns a value , so it can legally sit in the expr slot.
Step 3 — place it (assemble). Because it is expr, it goes before for:
clamped = [n if n > 0 else 0 for n in [ 3 , - 1 , 4 , - 5 ]]
# → [3, 0, 4, 0]
Why this step? Assembling here is also where the positioning trap is settled — the ternary is the expr slot, so it must sit before the for, per the mnemonic "Choose-before, Cut-after".
Verify: length stays 4 (a ternary never drops items). 3 → 3 , − 1 → 0 , 4 → 4 , − 5 → 0 . ✓
Common mistake The classic swap
Writing [n for n in xs if n > 0 else 0] is a SyntaxError . A filter if after for is not allowed to have an else. If you see else, it must live in expr, before the for.
Worked example Of the numbers 1–10, keep only multiples of 3, and label each as its square or "big"
Keep multiples of 3 from 1..10; for each survivor emit its square if it is under 30, else the string "big".
Forecast: which multiples of 3 in 1..10 have squares under 30?
Step 1 — the filter (Cut-after). Multiples of 3: if x % 3 == 0. Goes after for.
Why this step? "Keep only multiples of 3" is a dropping decision — some numbers must never enter the list at all. Dropping is always a filter if, and a filter if always sits after the for (mnemonic "Cut-after").
Step 2 — the ternary (Choose-before). For each survivor, x**2 if x**2 < 30 else "big". Goes before for.
Why this step? "Emit its square, else the string big" is a value-choice per surviving item, not a drop. A value-choice is a ternary, and a ternary lives in expr, which sits before the for (mnemonic "Choose-before").
Step 3 — assemble both. Two ifs, two positions:
out = [x ** 2 if x ** 2 < 30 else "big" for x in range ( 1 , 11 ) if x % 3 == 0 ]
# → [9, "big", "big"]
Why this step? This is the full "both ifs" shape. The one after for decides whether an item enters; the one before for decides what it becomes. Assembling proves the two ifs coexist without clashing precisely because they occupy different slots.
Verify: multiples of 3 in 1..10 are 3 , 6 , 9 . Squares: 9 , 36 , 81 . Only 9 < 30 , so → 9 ; 36 and 81 → "big". Output [ 9 , "big" , "big" ] , length 3. ✓
Worked example Flatten a 3×2 grid into one list
grid = [[1, 2], [3, 4], [5, 6]] → one flat list.
Forecast: guess the order of the six numbers.
Step 1 — write the honest nested loop first.
flat = []
for row in grid: # outer: [1,2], then [3,4], then [5,6]
for cell in row: # inner: each number in that row
flat.append(cell)
Why this step? Nested-for comprehensions read in the same order as the nested loops — outer first, inner second. Writing the loop pins the order.
Step 2 — collapse, keeping the loop order (assemble). The rule (from the parent note's equivalence law) is that a comprehension copies the loops left-to-right, in the same order you would type them as nested for statements . So the outer loop (for row in grid) is written first / leftmost , and the inner loop (for cell in row) is written second , exactly as they nest:
flat = [cell for row in grid for cell in row]
# → [1, 2, 3, 4, 5, 6]
Why this step? Assembling is where the ordering law bites: because the desugaring is a mechanical left-to-right rewrite — each for clause becomes one loop in the same order — the leftmost for must be the outermost loop. Swapping them (for cell in row for row in grid) raises a NameError, because row would be used before its loop defined it. That name-dependency is exactly why the assembled order is forced.
The figure below traces this: each grid row (colour-coded) empties left-to-right into the flat list, outer row-loop slow, inner cell-loop fast.
Figure: the 3×2 grid on the left; coloured arrows carry each row's cells (orange = row 0, teal = row 1, plum = row 2) into the single flat list on the right, in outer-to-inner order.
Verify: 3 rows × 2 cells = 6 items. Order follows the arrows in the figure: row [ 1 , 2 ] first, so 1 , 2 , then 3 , 4 , then 5 , 6 . ✓
Worked example Empty iterable and "filter kills everything"
Two edge cases most people never test.
Forecast: what does a comprehension over nothing return? A crash, None, or []?
Step 1 — empty iterable. range(0) yields no items at all.
a = [x ** 2 for x in range ( 0 )]
# → []
Why this step? If the for never picks an x, expr never runs — you get the empty list , never an error. This is the safe, graceful base case.
Step 2 — filter drops all. Non-empty source, but no item passes:
b = [n for n in [ 1 , 3 , 5 ] if n % 2 == 0 ]
# → []
Why this step? Odd numbers can never satisfy "even", so every item is dropped. Again [], not an error.
Verify: both are the empty list []. A comprehension always returns a list (possibly empty); it never returns None and never raises just because there is nothing to keep. ✓
Worked example Prices with tax, but only affordable items
A shop has prices (in ₹) [100, 250, 40, 999, 60]. Add 18% tax, but only include items whose pre-tax price is at most ₹200. Round tax-inclusive prices to whole rupees.
Forecast: which three prices survive the ₹200 cutoff?
Step 0 — anchor the tool round(). round(value) is a built-in Python function that returns the nearest whole number to value; on a .5 tie it rounds to the nearest even number (so round(2.5) is 2), but no ties arise here. Example: round(70.8) gives 71, round(47.2) gives 47.
Why this step? We must define every tool before using it — round is what turns 70.8 into a clean ₹ amount, and its exact behaviour on fractions decides the answer.
Step 1 — identify slots from the English sentence.
iterable: for p in [100, 250, 40, 999, 60]
filter: "pre-tax price at most ₹200" → if p <= 200
expr: "add 18% tax, round" → round(p * 1.18)
Why this step? Word problems are solved by matching each English clause to a slot. "Only include… if" is always the filter; "compute/transform" is always expr.
Step 2 — note the ordering trap. The filter uses the pre-tax p, and it runs before expr. So we filter on the raw price, then tax the survivors — exactly what the sentence says.
Why this step? Execution order is for → if → expr; the filter never sees the taxed value, which is what we want here.
Step 3 — assemble.
final = [ round (p * 1.18 ) for p in [ 100 , 250 , 40 , 999 , 60 ] if p <= 200 ]
# → [118, 47, 71]
Why this step? With the tool anchored and the slots matched, assembling fuses filter and rounded transform into the single line that answers the word problem.
Verify: survivors (≤200): 100 , 40 , 60 . Taxed: 100 × 1.18 = 118 , 40 × 1.18 = 47.2 → 47 , 60 × 1.18 = 70.8 → 71 . Dropped: 250 , 999 . Units stay ₹ throughout. Output [ 118 , 47 , 71 ] . ✓
Worked example Even-sum pairs
From two ranges, build all pairs (i, j) with i in 1..3, j in 1..3, keeping only pairs where i + j is even. Emit the product i*j.
Forecast: how many of the nine possible pairs have an even sum?
Step 1 — the two loops. Outer for i in range(1, 4), inner for j in range(1, 4). Both give 1 , 2 , 3 .
Why this step? Two fors = nested loops, outer written first (Case F rule), giving 3 × 3 = 9 candidate pairs.
Step 2 — the filter depends on both loop variables. if (i + j) % 2 == 0. This is legal — a filter may use any variable already introduced to its left.
Why this step? The subtle exam point: the filter is evaluated inside the innermost loop , where both i and j are known, so it can combine them.
Step 3 — expr. The product i * j.
Why this step? The problem says "emit the product i*j", so the output expression that lands in the new list must be i * j — computed last, only for pairs that passed the even-sum filter.
Step 4 — assemble.
out = [i * j for i in range ( 1 , 4 ) for j in range ( 1 , 4 ) if (i + j) % 2 == 0 ]
Why this step? All slots are now decided (two fors in outer-to-inner order, one two-variable filter, one product expr), so we place them into the template — assembling is the mechanical final move that turns the reasoning into one runnable line.
Step 5 — trace in exact loop order. The outer i moves slowly; for each i the inner j sweeps 1 , 2 , 3 . We keep a pair only when i + j is even:
i = 1 : j = 1 ⇒ sum 2 even ✓ product 1 ; j = 2 ⇒ sum 3 odd ✗; j = 3 ⇒ sum 4 even ✓ product 3 .
i = 2 : j = 1 ⇒ sum 3 odd ✗; j = 2 ⇒ sum 4 even ✓ product 4 ; j = 3 ⇒ sum 5 odd ✗.
i = 3 : j = 1 ⇒ sum 4 even ✓ product 3 ; j = 2 ⇒ sum 5 odd ✗; j = 3 ⇒ sum 6 even ✓ product 9 .
Reading the ✓ products in loop order gives out = [1, 3, 4, 3, 9].
Verify: 5 pairs survive out of 9, and the order follows outer-i-slow / inner-j-fast: ( 1 , 1 ) , ( 1 , 3 ) , ( 2 , 2 ) , ( 3 , 1 ) , ( 3 , 3 ) → products [ 1 , 3 , 4 , 3 , 9 ] . ✓
Worked example Numbers divisible by BOTH 2 and 3
From 1..20, keep numbers that are even and divisible by 3, using two separate if clauses.
Forecast: which numbers in 1..20 are divisible by both 2 and 3?
Step 1 — iterable. range(1, 21) gives 1 , 2 , … , 20 .
Why this step? The source the for slot draws from.
Step 2 — recognise you may chain filters. Python allows more than one filter if after a single for, written back-to-back with no and and no else: ... if cond1 if cond2. They behave like cond1 and cond2 — an item survives only if every clause is True.
Why this step? This is the valid "multiple filter" case. Two ifs in a row is not a syntax error (unlike an if...else after for); it is just chained filtering, evaluated left-to-right.
Step 3 — write both filters. Even: if x % 2 == 0. Divisible by 3: if x % 3 == 0.
Why this step? Each condition is a separate clause; chaining them means an item must clear the first before the second is even tested.
Step 4 — assemble.
out = [x for x in range ( 1 , 21 ) if x % 2 == 0 if x % 3 == 0 ]
# → [6, 12, 18]
Why this step? With expr = x and two chained filters decided, assembling places them into [expr for x in iterable if cond1 if cond2] — the mechanical final step that shows chained ifs live side by side after the single for.
Verify: divisible by both 2 and 3 means divisible by 6. Multiples of 6 in 1..20 : 6 , 12 , 18 . Output [ 6 , 12 , 18 ] . ✓ (Equivalent one-filter form if x % 2 == 0 and x % 3 == 0 gives the same list.)
Recall Which cell was which?
Match example → matrix cell.
Example 4 (n if n>0 else 0) hits which case? ::: Case D — ternary in expr, if/else before for.
Example 7 hits which case? ::: Case G — degenerate inputs, result is [].
Example 9's filter uses both i and j — is that legal? ::: Yes; a filter may use any variable introduced to its left (Case I).
In Example 8, does the filter see the pre-tax or post-tax price? ::: Pre-tax — the filter runs before expr.
Are two ifs in a row after one for allowed? ::: Yes — chained filters (Case J), behaving like and.
Mnemonic The ten-cell checklist
Before shipping a comprehension, ask: transform? filter? both? ternary? nested? chained filters? could it be empty? If you can answer each, you have covered A–J.
For loops — every example was first written as an honest loop, then collapsed.
Conditional expressions (ternary) — the a if c else b in Examples 4 and 5.
map() and filter() — Examples 1–2 are map/filter in disguise.
Dictionary and set comprehensions — same cells, different brackets.
Generator expressions — swap [] for () to get these results lazily.
range() and iterables — feeds the for slot in nearly every example.
The diagram below is a pure recap: the central node is the scenario matrix, and each branch is one case class (A–J) with the example that covers it. Use it as a checklist — if you can name the trick each branch tests, you have covered the whole territory.
C transform plus filter - Ex3
E ternary and filter - Ex5