Session 17 of the Python Fundamentals series, covered in depth. It builds a whole list on one line with a comprehension, covering the [expr for x in it] form, adding a filter with if, and rewriting an accumulate-in-a-loop pattern as a comprehension. It then covers dictionary comprehensions, set comprehensions that drop duplicates, and nested iteration with two for clauses. The traps are that the comprehension variable does NOT leak the way a for-loop variable does, that running a comprehension only for its side effects wastes a list of None, and that an over-nested one-liner is less clear than the loop it replaced. Each idea is shown side by side with the equivalent explicit loop in a trace table. Every snippet and error message was executed and copied verbatim from CPython 3.12.
Subject: Python Fundamentals · 102 slides · code lesson
Open the interactive version of this deck · Homework for this lesson
Title
Python Fundamentals - Session 17
Build a whole list, dict, or set in one readable line
Objectives
You already build lists with a for loop and append. A comprehension does the same job in one line. By the end you can:
[expr for x in it] and add a filter with if.{k: v for ...} and a set with {expr for ...}.Warm-up
Discussion prompt
Before we open Session 17 - Comprehensions: without looking back, what was the main idea of Session 16 - Sets & Set Operations, and what could you do by the end of it that you could not do before?
Hint: One sentence for the idea, one for the skill. If the second one is blank, that is the part to revisit.
Answer:
Session 16 of the Python Fundamentals series, in depth. Sets as unordered collections of unique items: building them with {1, 2, 3} and set(), why the empty set is set() and never {}, adding with .add, removing safely with .discard versus .remove, fast membership with in, deduplicating a list with set(list), and the four combining operators - union |, intersection &, difference -, and symmetric difference ^.
Section
Part 1
Concept
A list comprehension walks a sequence and collects one new value per item - all inside a pair of square brackets.
list comprehension — A compact expression that builds a list: [expression for item in iterable]. It runs the expression once per item and collects the results.
Counterexample
Discussion prompt
A list comprehension walks a sequence and collects one new value per item - all inside a pair of square brackets.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Concept
The shape is [expr for x in it]. The for x in it part is the loop; expr is what you keep each time.
Read it aloud: 'give me expr, for each x in it'. The loop drives it; the expression shapes each result.
Analogy
Discussion prompt
Explain Read it as expression, then for by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
The shape is [expr for x in it]. The for x in it part is the loop; expr is what you keep each time.
Ranking
Put in order
Put the moves of Squares of 0 to 4 into the order they have to happen.
Why: These are the moves of the worked example in the order it makes them, and each one is set up by the one before it. range(5) yields those five values, one at a time.
Worked example
squares = [n * n for n in range(5)]
print(squares)The loop walks n over 0,1,2,3,4
Why: range(5) yields those five values, one at a time.
Each n contributes n * n to the new list
Why: The expression n * n runs once per value and the result is collected in order.
Read the output
Why: Verified by execution: [0, 1, 4, 9, 16].
| n | n * n | list so far |
|---|---|---|
| 0 | 0 | [0] |
| 1 | 1 | [0, 1] |
| 2 | 4 | [0, 1, 4] |
| 3 | 9 | [0, 1, 4, 9] |
| 4 | 16 | [0, 1, 4, 9, 16] |
Comparison
Comparison matrix
From Squares of 0 to 4: refill the **n * n** column from what you know. The rest of the table is as it appeared.
| n | n * n | list so far |
|---|---|---|
| 0 | 0 | [0] |
| 1 | 1 | [0, 1] |
| 2 | 4 | [0, 1, 4] |
| 3 | 9 | [0, 1, 4, 9] |
| 4 | 16 | [0, 1, 4, 9, 16] |
Pattern
Predict first
The table runs: 1 | 2 | [2] · 2 | 4 | [2, 4] · 3 | 6 | [2, 4, 6] · 4 | 8 | [2, 4, 6, 8]
In Double every number, given the rows so far: what is the next one — the row where n is 5?
Correct: 5 | 10 | [2, 4, 6, 8, 10]
| n | n * 2 | list so far |
|---|---|---|
| 1 | 2 | [2] |
| 2 | 4 | [2, 4] |
| 3 | 6 | [2, 4, 6] |
| 4 | 8 | [2, 4, 6, 8] |
| 5 | 10 | [2, 4, 6, 8, 10] |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. The iterable does not have to be a range - any list works.
Worked example
nums = [1, 2, 3, 4, 5]
doubled = [n * 2 for n in nums]
print(doubled)n walks the existing list
Why: The iterable does not have to be a range - any list works.
Each item is multiplied by 2
Why: Verified by execution: [2, 4, 6, 8, 10].
| n | n * 2 | list so far |
|---|---|---|
| 1 | 2 | [2] |
| 2 | 4 | [2, 4] |
| 3 | 6 | [2, 4, 6] |
| 4 | 8 | [2, 4, 6, 8] |
| 5 | 10 | [2, 4, 6, 8, 10] |
Trade off
Comparison matrix
From Double every number: every row here is a choice with a cost. Fill the **n * 2** column, then say which row you would actually pick and what you give up for it.
| n | n * 2 | list so far |
|---|---|---|
| 1 | 2 | [2] |
| 2 | 4 | [2, 4] |
| 3 | 6 | [2, 4, 6] |
| 4 | 8 | [2, 4, 6, 8] |
| 5 | 10 | [2, 4, 6, 8, 10] |
Intuition
The part before for is just an expression evaluated for each item. It can call a function, index a string, do math - whatever produces the value you want to keep.
The item name (n, w, x) is yours to pick, exactly like the variable in a for loop.
Explain it
Discussion prompt
Explain The expression can be anything to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
The part before for is just an expression evaluated for each item. It can call a function, index a string, do math - whatever produces the value you want to keep.
Fill the middle
Fill in the blanks
From Length of each word — one line has had its right-hand side removed. Put it back.
words = ["hi", "there", "you"]
lengths = [len(w) for w in words]
print(lengths)
Why: lengths is what everything below it consumes, so the wrong expression here fails later and somewhere else. You can call any function on the item to build each result.
Worked example
words = ["hi", "there", "you"]
lengths = [len(w) for w in words]
print(lengths)The expression is len(w)
Why: You can call any function on the item to build each result.
Read the output
Why: Verified by execution: [2, 5, 3].
| w | len(w) | list so far |
|---|---|---|
| "hi" | 2 | [2] |
| "there" | 5 | [2, 5] |
| "you" | 3 | [2, 5, 3] |
Pattern
Step through it
Step through Length of each word one row at a time. What is driving the change, and what would the row after the last one be?
Worked example
names = ["ann", "bob", "cid"]
initials = [name[0] for name in names]
print(initials)The expression indexes each string
Why: name[0] grabs the first character of each name.
Read the output
Why: Verified by execution: ['a', 'b', 'c'].
| name | name[0] | initials |
|---|---|---|
| "ann" | 'a' | ['a'] |
| "bob" | 'b' | ['a', 'b'] |
| "cid" | 'c' | ['a', 'b', 'c'] |
Blank canvas
Draw it
Draw what The first letter of each name just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Section
Part 2
Concept
You know this shape well: start an empty list, loop, and append one value each pass.
Whenever a loop does only that - build one list by appending a single expression - it can become a comprehension.
Worked example
squares = []
for n in range(5):
squares.append(n * n)
print(squares)Three moving parts: empty list, loop, append
Why: squares starts empty; each pass appends n * n.
Read the output
Why: Verified by execution: [0, 1, 4, 9, 16] - the same result as the comprehension.
| n | append | squares |
|---|---|---|
| 0 | 0 | [0] |
| 1 | 1 | [0, 1] |
| 2 | 4 | [0, 1, 4] |
| 3 | 9 | [0, 1, 4, 9] |
| 4 | 16 | [0, 1, 4, 9, 16] |
Error analysis
Annotate
Walk the callouts on The loop version first. Each one is a place this is easy to get subtly wrong.
Concept
Only convert a loop whose whole job is 'append one expression per item'. That is the pattern a comprehension replaces exactly.
A loop that keeps a running total, updates other variables, or does several things is not a comprehension - leave it a loop.
Concept
The append(EXPR) becomes the EXPR at the front; the for line moves inside the brackets. The empty list and .append disappear.
[n * n for n in range(5)] says the same thing as those three lines - with less to get wrong.
Worked example
prices = [10, 25, 5, 40]
with_fee = [p + 3 for p in prices]
print(with_fee)Mentally the loop was: total_list = []; for p in prices: total_list.append(p + 3)
Why: The comprehension collapses that empty-list-plus-append into one line.
Both forms give the identical list
Why: Verified by execution: [13, 28, 8, 43] from either the loop or the comprehension.
| p | p + 3 | with_fee |
|---|---|---|
| 10 | 13 | [13] |
| 25 | 28 | [13, 28] |
| 5 | 8 | [13, 28, 8] |
| 40 | 43 | [13, 28, 8, 43] |
Section
Part 3
Concept
Put an if after the loop: [expr for x in it if cond]. Only items where cond is true reach the expression.
filter clause — The optional if cond at the end of a comprehension. When it is false the item is skipped entirely - nothing is added for it.
Definition probe
Sort into buckets
Every line below is part of the definition of list comprehension or of filter clause — one or the other, never both. Put each where it belongs.
if cond at the end of a comprehension.; When it is false the item is skipped entirely - nothing is added for it.if cond at the end of a comprehension. When it is false the item is skipped entirely - nothing is added for it.Worked example
nums = [4, 7, 10, 3, 8]
evens = [n for n in nums if n % 2 == 0]
print(evens)The if runs for each n before anything is kept
Why: n % 2 == 0 is the test; only passing values are added.
Odd numbers are skipped, not turned into anything
Why: Verified by execution: [4, 10, 8].
| n | n % 2 == 0 | kept? | evens |
|---|---|---|---|
| 4 | True | yes | [4] |
| 7 | False | no | [4] |
| 10 | True | yes | [4, 10] |
| 3 | False | no | [4, 10] |
| 8 | True | yes | [4, 10, 8] |
Scale up
Step through it
Step through Keep the even numbers and watch the numbers move. Now imagine the input ten times bigger: which column is the one that stops this being practical?
Worked example
scores = [55, 82, 90, 47, 73]
passing = [s for s in scores if s >= 60]
print(passing)The filter is a condition, just like in an if statement
Why: s >= 60 decides which scores survive.
Read the output
Why: Verified by execution: [82, 90, 73].
| s | s >= 60 | passing |
|---|---|---|
| 55 | False | [] |
| 82 | True | [82] |
| 90 | True | [82, 90] |
| 47 | False | [82, 90] |
| 73 | True | [82, 90, 73] |
Sorting
Sort into buckets
These are the pieces of Session 17 - Comprehensions, out of order. Put each one back under the part of the lesson it belongs to.
Concept
The expression at the front and the if at the back are independent. You can reshape the survivors while dropping the rest.
Worked example
nums = [1, 2, 3, 4, 5, 6]
even_squares = [n * n for n in nums if n % 2 == 0]
print(even_squares)First the if decides, then the expression shapes
Why: Odd values never reach n * n; even ones get squared.
Read the output
Why: Verified by execution: [4, 16, 36].
| n | even? | n * n | even_squares |
|---|---|---|---|
| 1 | no | - | [] |
| 2 | yes | 4 | [4] |
| 3 | no | - | [4] |
| 4 | yes | 16 | [4, 16] |
| 6 | yes | 36 | [4, 16, 36] |
Pattern
Step through it
Step through Square only the even numbers one row at a time. What is driving the change, and what would the row after the last one be?
Worked example
nums = [1, 3, 5]
evens = [n for n in nums if n % 2 == 0]
print(evens)No item passes the test
Why: Every value is odd, so nothing is ever added.
You get an empty list, not an error
Why: Verified by execution: [].
| n | n % 2 == 0 | evens |
|---|---|---|
| 1 | False | [] |
| 3 | False | [] |
| 5 | False | [] |
Section
Part 4
Concept
Swap the square brackets for curly braces and give a key: value pair: {k: v for ...}. Each pass adds one entry.
dict comprehension — Builds a dictionary: {key: value for item in iterable}. The colon separates the key expression from the value expression.
Worked example
names = ["ann", "bob", "cid"]
lengths = {name: len(name) for name in names}
print(lengths)The key is name, the value is len(name)
Why: Each item becomes one key-value entry.
Read the output
Why: Verified by execution: {'ann': 3, 'bob': 3, 'cid': 3}.
| name | key | value | dict so far |
|---|---|---|---|
| "ann" | 'ann' | 3 | {'ann': 3} |
| "bob" | 'bob' | 3 | {'ann': 3, 'bob': 3} |
| "cid" | 'cid' | 3 | {'ann': 3, 'bob': 3, 'cid': 3} |
Worked example
prices = {"pen": 2, "pad": 5, "ink": 8}
doubled = {item: cost * 2 for item, cost in prices.items()}
print(doubled).items() unpacks into item and cost each pass
Why: This is the same unpacking you use when looping over a dict.
Keep the key, rebuild the value
Why: Verified by execution: {'pen': 4, 'pad': 10, 'ink': 16}.
| item | cost | cost * 2 | doubled |
|---|---|---|---|
| "pen" | 2 | 4 | {'pen': 4} |
| "pad" | 5 | 10 | {'pen': 4, 'pad': 10} |
| "ink" | 8 | 16 | {'pen': 4, 'pad': 10, 'ink': 16} |
Fill the middle
Fill in the blanks
From A number-to-square lookup — one line has had its right-hand side removed. Put it back.
nums = [1, 2, 3, 4]
squares = **{n: n * n for n in nums}**
print(squares)
Why: squares is what everything below it consumes, so the wrong expression here fails later and somewhere else. The key is n; the value is n * n computed from the same n.
Worked example
nums = [1, 2, 3, 4]
squares = {n: n * n for n in nums}
print(squares)One iterable feeds both the key and the value
Why: The key is n; the value is n * n computed from the same n.
Read the output
Why: Verified by execution: {1: 1, 2: 4, 3: 9, 4: 16}.
| n | key | value | squares |
|---|---|---|---|
| 1 | 1 | 1 | {1: 1} |
| 2 | 2 | 4 | {1: 1, 2: 4} |
| 3 | 3 | 9 | {1: 1, 2: 4, 3: 9} |
| 4 | 4 | 16 | {1: 1, 2: 4, 3: 9, 4: 16} |
Worked example
scores = {"ann": 90, "bob": 45, "cid": 72}
passed = {name: sc for name, sc in scores.items() if sc >= 60}
print(passed)The if works here too, at the end
Why: Entries whose value fails the test are dropped.
bob is filtered out
Why: Verified by execution: {'ann': 90, 'cid': 72}.
| name | sc | sc >= 60 | kept? |
|---|---|---|---|
| "ann" | 90 | True | yes |
| "bob" | 45 | False | no |
| "cid" | 72 | True | yes |
Worked example
words = ["ann", "bob", "cid", "dan"]
by_len = {len(w): w for w in words}
print(by_len)Every word here has length 3
Why: So each pass writes to the same key, 3, overwriting the last value.
Only the final write survives
Why: Verified by execution: {3: 'dan'} - a dict keeps one value per key, and later pairs replace earlier ones.
| w | len(w) | by_len |
|---|---|---|
| "ann" | 3 | {3: 'ann'} |
| "bob" | 3 | {3: 'bob'} |
| "cid" | 3 | {3: 'cid'} |
| "dan" | 3 | {3: 'dan'} |
Section
Part 5
Concept
{expr for ...} - curly braces but a single expression, no key: value. That builds a set, so duplicates collapse automatically.
set comprehension — Builds a set: {expression for item in iterable}. Like a list comprehension but the result keeps only distinct values.
Matching
Match the pairs
Match each term to the definition this lesson gave it — not the one you would guess from the word.
if cond at the end of a comprehension. When it is false the item is skipped entirely - nothing is added for it.Why: These are the working definitions of list comprehension, filter clause, dict comprehension, set comprehension as Session 17 - Comprehensions uses them. Pairing them correctly is the test of whether you could state each one with the slide switched off.
Worked example
nums = [1, 2, 2, 3, 3, 3]
unique_squares = {n * n for n in nums}
print(unique_squares)Repeated inputs make repeated results
Why: 1 appears once, 2 twice, 3 three times - so does each square before the set collapses them.
The set keeps each value once
Why: Verified by execution: {1, 4, 9}.
| n | n * n | set so far |
|---|---|---|
| 1 | 1 | {1} |
| 2 | 4 | {1, 4} |
| 2 | 4 | {1, 4} |
| 3 | 9 | {1, 4, 9} |
| 3 | 9 | {1, 4, 9} |
Comparison
Comparison matrix
From Distinct squares: refill the set so far column from what you know. The rest of the table is as it appeared.
| n | n * n | set so far |
|---|---|---|
| 1 | 1 | {1} |
| 2 | 4 | {1, 4} |
| 2 | 4 | {1, 4} |
| 3 | 9 | {1, 4, 9} |
| 3 | 9 | {1, 4, 9} |
Worked example
words = ["a", "bb", "cc", "ddd"]
sizes = {len(w) for w in words}
print(sizes)Two words share length 2
Why: "bb" and "cc" both give 2, but the set stores 2 only once.
Read the output
Why: Verified by execution: {1, 2, 3}.
| w | len(w) | sizes |
|---|---|---|
| "a" | 1 | {1} |
| "bb" | 2 | {1, 2} |
| "cc" | 2 | {1, 2} |
| "ddd" | 3 | {1, 2, 3} |
Discrimination
Sort into buckets
Sort these by len(w), from memory, without looking back at The distinct word lengths. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Intuition
Curly braces do double duty. A key: value inside means a dict; a bare expression means a set. The colon is the whole difference.
A set has no duplicates and no fixed order, so reach for it when you want the distinct values and do not care about position.
Section
Part 6
Concept
You can chain loops: [expr for a in first for b in second]. The left for is the outer loop, the right one is inner.
Read them in the same order you would write nested for statements - top to bottom becomes left to right.
Worked example
pairs = [(x, y) for x in [1, 2] for y in [10, 20]]
print(pairs)For each x, y runs through the whole second list
Why: x is the outer loop; y cycles fully before x advances.
Read the output
Why: Verified by execution: [(1, 10), (1, 20), (2, 10), (2, 20)].
| x | y | pair | pairs so far |
|---|---|---|---|
| 1 | 10 | (1, 10) | [(1, 10)] |
| 1 | 20 | (1, 20) | [(1, 10), (1, 20)] |
| 2 | 10 | (2, 10) | [(1, 10), (1, 20), (2, 10)] |
| 2 | 20 | (2, 20) | [(1, 10), (1, 20), (2, 10), (2, 20)] |
Worked example
grid = [[1, 2, 3], [4, 5, 6]]
flat = [n for row in grid for n in row]
print(flat)Outer loop picks a row, inner loop picks each number
Why: for row in grid runs first; for n in row runs inside it.
The two levels become one flat list
Why: Verified by execution: [1, 2, 3, 4, 5, 6].
| row | n | flat so far |
|---|---|---|
| [1, 2, 3] | 1 | [1] |
| [1, 2, 3] | 2 | [1, 2] |
| [1, 2, 3] | 3 | [1, 2, 3] |
| [4, 5, 6] | 4 | [1, 2, 3, 4] |
| [4, 5, 6] | 6 | [1, 2, 3, 4, 5, 6] |
Pattern
Predict first
The table runs: [1, 2] | 1 | no | [] · [1, 2] | 2 | yes | [2] · [3, 4] | 4 | yes | [2, 4]
In Flatten and filter at once, given the rows so far: what is the next one — the row where row is [5, 6]?
Correct: [5, 6] | 6 | yes | [2, 4, 6]
| row | n | even? | flat_evens |
|---|---|---|---|
| [1, 2] | 1 | no | [] |
| [1, 2] | 2 | yes | [2] |
| [3, 4] | 4 | yes | [2, 4] |
| [5, 6] | 6 | yes | [2, 4, 6] |
Why: The relationship between the columns, not the individual numbers, is what generates the next row. Each n is tested after both loops pick it out.
Worked example
grid = [[1, 2], [3, 4], [5, 6]]
flat_evens = [n for row in grid for n in row if n % 2 == 0]
print(flat_evens)The if applies to the innermost item
Why: Each n is tested after both loops pick it out.
Only even numbers survive the flatten
Why: Verified by execution: [2, 4, 6].
| row | n | even? | flat_evens |
|---|---|---|---|
| [1, 2] | 1 | no | [] |
| [1, 2] | 2 | yes | [2] |
| [3, 4] | 4 | yes | [2, 4] |
| [5, 6] | 6 | yes | [2, 4, 6] |
Pattern
Step through it
Step through Flatten and filter at once one row at a time. What is driving the change, and what would the row after the last one be?
Section
Part 7
Concept
A comprehension shines when the whole job is 'make a new list/dict/set from an old one'. That is its sweet spot.
The moment the body needs several statements, running state, or heavy branching, a plain loop reads better.
Explain it
Discussion prompt
Explain Comprehensions are for building collections to a student a year behind you. No notation, no jargon they have not met — and it still has to be true.
Hint: If your explanation needs a symbol they have never seen, you are describing the notation rather than the idea.
Answer:
A comprehension shines when the whole job is 'make a new list/dict/set from an old one'. That is its sweet spot.
Worked example
prices = [10, 25, 5, 40]
labels = ["expensive" if p >= 20 else "cheap" for p in prices]
print(labels)This if is inside the expression, not a filter
Why: "expensive" if p >= 20 else "cheap" picks the value; it comes before the for.
Still readable at one condition
Why: Verified by execution: ['cheap', 'expensive', 'cheap', 'expensive'].
| p | p >= 20 | label | labels |
|---|---|---|---|
| 10 | False | cheap | ['cheap'] |
| 25 | True | expensive | ['cheap', 'expensive'] |
| 5 | False | cheap | ['cheap', 'expensive', 'cheap'] |
| 40 | True | expensive | ['cheap', 'expensive', 'cheap', 'expensive'] |
Concept
Readability beats cleverness. A comprehension you have to decode line by line is worse than a four-line loop anyone can follow.
When the body grows - logging, multiple conditions, extra variables - expand it back to a for loop on purpose.
Analogy
Discussion prompt
Explain If you cannot read it, write the loop by analogy to something with no Python Fundamentals in it at all — a queue, a recipe, a map, a bank balance, whatever fits. Then say where your analogy breaks.
Hint: An analogy that never breaks is not an analogy, it is the same idea wearing a hat. Find the seam — that is the part that is actually new.
Answer:
Readability beats cleverness. A comprehension you have to decode line by line is worse than a four-line loop anyone can follow.
Fill the middle
Fill in the blanks
From The same labels, spelled out as a loop — one line has had its right-hand side removed. Put it back.
prices = [10, 25, 5, 40]
labels = []
for p in prices:
if p >= 20:
labels.append("expensive")
else:
labels.append("cheap")
print(labels)
Why: prices is what everything below it consumes, so the wrong expression here fails later and somewhere else. When the logic might grow, the loop gives it room to breathe.
Worked example
prices = [10, 25, 5, 40]
labels = []
for p in prices:
if p >= 20:
labels.append("expensive")
else:
labels.append("cheap")
print(labels)Longer, but every step is on its own line
Why: When the logic might grow, the loop gives it room to breathe.
Identical result to the comprehension
Why: Verified by execution: ['cheap', 'expensive', 'cheap', 'expensive'].
| p | branch | labels |
|---|---|---|
| 10 | else | ['cheap'] |
| 25 | if | ['cheap', 'expensive'] |
| 5 | else | ['cheap', 'expensive', 'cheap'] |
| 40 | if | ['cheap', 'expensive', 'cheap', 'expensive'] |
Blank canvas
Draw it
Draw what The same labels, spelled out as a loop just did — the shape of it, not the line-by-line working. One picture, labels only where you need them. Then check it against the steps: anything you could not draw is a step you followed rather than understood.
Section
Part 8
Anomaly
Predict first
A student writes this, and it looks reasonable:
You expect the loop variable to survive, like it does after a for loop.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: The comprehension has its own private scope, so n is gone the moment it finishes - the last print crashes.
Use the list you built - or a real for loop if you truly need the variable afterward.
Why: The comprehension has its own private scope, so n is gone the moment it finishes - the last print crashes.
Trap
You expect the loop variable to survive, like it does after a for loop.
squares = [n * n for n in range(5)]
print(squares)
print(n)n exists only inside the comprehension
Why: The comprehension has its own private scope, so n is gone the moment it finishes - the last print crashes.
| line | result |
|---|---|
| print(squares) | [0, 1, 4, 9, 16] |
| print(n) | NameError: name 'n' is not defined |
Use the list you built - or a real for loop if you truly need the variable afterward.
for n in range(5):
pass
print(n)A for loop DOES leave n behind
Why: Real output: 4. A for-loop variable survives after the loop; a comprehension variable does not. Do not rely on a comprehension to hand you its variable.
| construct | is n defined after? |
|---|---|
| comprehension | no (NameError) |
| for loop | yes (holds last value 4) |
Error analysis
Annotate
Walk the callouts on Trap: the comprehension variable does not leak. Each one is a place this is easy to get subtly wrong.
Concept
Because the name stays inside, a comprehension can never accidentally clobber a variable you already have. n outside is safe.
So the fix is never 'reach for the leaked variable' - it is 'use the collection you built'.
Counterexample
Discussion prompt
Because the name stays inside, a comprehension can never accidentally clobber a variable you already have. n outside is safe.
That is stated as though it always holds. Do one of two things: produce a case where it fails, or say precisely what rules such a case out. "It just does" is not on the menu.
Hint: Hunt at the extremes first — zero, one, negative, empty, equal. If every extreme survives, the reason they survive is the proof.
Anomaly
Predict first
A student writes this, and it looks reasonable:
Using a comprehension just to print - throwing the result away.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: It prints ann then bob, but also collects each print's return value.
If you only want the side effect, write a plain for loop.
Why: It prints ann then bob, but also collects each print's return value. result is a useless list of None that wasted memory.
Trap
Using a comprehension just to print - throwing the result away.
names = ["ann", "bob"]
result = [print(name) for name in names]
print(result)print returns None, so you build [None, None]
Why: It prints ann then bob, but also collects each print's return value. result is a useless list of None that wasted memory.
| step | output / value |
|---|---|
| name = "ann" | prints ann, collects None |
| name = "bob" | prints bob, collects None |
| result | [None, None] |
If you only want the side effect, write a plain for loop.
names = ["ann", "bob"]
for name in names:
print(name)A loop makes the intent obvious and builds nothing
Why: Real output: ann then bob, with no leftover list. Rule of thumb: a comprehension is for the value it returns - if you ignore that value, use a loop.
| approach | screen | builds a list? |
|---|---|---|
| comprehension | ann / bob | yes - [None, None] wasted |
| for loop | ann / bob | no |
Cost model
Annotate
In Trap: a comprehension only for side effects, before reading the notes: mark where the time actually goes. Which line dominates?
Anomaly
Predict first
A student writes this, and it looks reasonable:
Cramming two loops and a filter into one dense line.
It is wrong. Say what breaks — and say it before you turn the page.
Correct: It works - output [2, 3, 2, 6, 3, 6] - yet a reader has to untangle three clauses on one line to see what it does.
Spell the nesting out as loops when it gets busy.
Why: It works - output [2, 3, 2, 6, 3, 6] - yet a reader has to untangle three clauses on one line to see what it does.
Trap
Cramming two loops and a filter into one dense line.
products = [x * y for x in range(1, 4) for y in range(1, 4) if x != y]
print(products)Correct, but hard to scan
Why: It works - output [2, 3, 2, 6, 3, 6] - yet a reader has to untangle three clauses on one line to see what it does.
| x | y | x != y | kept |
|---|---|---|---|
| 1 | 2 | True | 2 |
| 1 | 3 | True | 3 |
| 2 | 1 | True | 2 |
| 2 | 3 | True | 6 |
| 3 | 1 | True | 3 |
Spell the nesting out as loops when it gets busy.
products = []
for x in range(1, 4):
for y in range(1, 4):
if x != y:
products.append(x * y)
print(products)Same result, far easier to read and change
Why: Real output: [2, 3, 2, 6, 3, 6] - identical to the one-liner, but each loop and the condition are on their own line.
| form | output | readable? |
|---|---|---|
| nested one-liner | [2, 3, 2, 6, 3, 6] | hard |
| nested for loops | [2, 3, 2, 6, 3, 6] | easy |
Two truths and a lie
Sort into buckets
Some of these hold up and some are the exact mistakes this lesson is built to prevent. Sort them.
[expr for x in it]. The for x in it part is the loop; expr is what you keep each time.; The part before for is just an expression evaluated for each item. It can call a function, index a string, do math - whatever produces the value you want to keep.Section
Part 9
Pattern
1. Start from the loop: empty collection, for, add one value
Why: If the loop only appends a single expression, it converts cleanly.
2. Put the added expression first, then the for clause
Why: [expr for x in it] - the append expression moves to the front.
3. Add if cond at the end to keep only some items
Why: [expr for x in it if cond] skips items that fail the test.
4. Pick the brackets for the result you want
Why: [] list, {k: v ...} dict, {expr ...} set - the shape of the output.
Pattern
Building one collection from another? comprehension
Why: That is exactly what it is for, and it reads as one clear line.
Only doing a side effect (print, write)? loop
Why: Ignoring the result means the built list is wasted - a loop says what you mean.
Body needs branches, state, or many steps? loop
Why: Readability wins; do not force a busy body onto one line.
Need the variable afterward? loop
Why: A comprehension's variable is private and vanishes when it ends.
Real world
Discussion prompt
Outside this lesson: where does Session 17 - Comprehensions actually turn up? Name one concrete situation — a job, a piece of software someone ships, a decision somebody has to make — and say which part of Comprehension or loop? is doing the work in it.
Hint: Vague is the failure mode here. "Engineering" is not a situation; "deciding whether this build is fast enough to ship" is.
Answer:
Session 17 of the Python Fundamentals series, in depth. Building a whole list in one line with a comprehension: [expr for x in it], adding a filter with if, and rewriting an accumulate-in-a-loop pattern as a comprehension.
Check
Predict the list before clicking.
words = ["cat", "dog", "bird"]
caps = [w.upper() for w in words]
print(caps)| w | w.upper() |
|---|---|
| "cat" | ? |
| "dog" | ? |
| "bird" | ? |
Check your understanding
What does this print?
Answer: A
Why: The expression w.upper() runs on each word and the results are collected in order, giving ['CAT', 'DOG', 'BIRD']. Verified by execution.
Check
The if runs before anything is kept.
nums = [1, 2, 3, 4]
result = [n + 1 for n in nums if n > 2]
print(result)| n | n > 2 | n + 1 (if kept) |
|---|---|---|
| 1 | False | - |
| 2 | False | - |
| 3 | True | ? |
| 4 | True | ? |
Check your understanding
What does this print?
Answer: A
Why: Only 3 and 4 pass n > 2; each then becomes n + 1, giving [4, 5]. The filter drops 1 and 2 entirely. Verified by execution.
Scale up
Step through it
Step through Check: filter clause and watch the numbers move. Now imagine the input ten times bigger: which column is the one that stops this being practical?
Check
Watch the colon - key on the left, value on the right.
d = {x: x * 10 for x in [1, 2, 3]}
print(d)| x | key | value |
|---|---|---|
| 1 | 1 | ? |
| 2 | 2 | ? |
| 3 | 3 | ? |
Check your understanding
What does this print?
Answer: A
Why: Each x becomes a key with value x * 10, so the dict is {1: 10, 2: 20, 3: 30}. The colon marks a dict comprehension. Verified by execution.
Invariant
Step through it
Step through Check: dict comprehension one row at a time. One of these columns never changes — find it, and say why it cannot.
Check
Two of these words share a length.
words = ["a", "bb", "cc", "ddd"]
sizes = {len(w) for w in words}
print(sizes)| w | len(w) |
|---|---|
| "a" | 1 |
| "bb" | 2 |
| "cc" | 2 |
| "ddd" | 3 |
Check your understanding
What does this print?
Answer: A
Why: The lengths are 1, 2, 2, 3, but a set stores each value once, so the duplicate 2 collapses to give {1, 2, 3}. Verified by execution.
Discrimination
Sort into buckets
Sort these by len(w), from memory, without looking back at Check: set comprehension. Telling them apart on the spot is the skill; the table is only where the answer happens to be written down.
Check
Think about where n lives.
squares = [n * n for n in range(3)]
print(n)| after the comprehension | is n defined? |
|---|---|
| print(n) | ? |
Check your understanding
What happens on the last line?
Answer: A
Why: A comprehension has its own private scope, so n does not exist outside it; print(n) raises NameError: name 'n' is not defined. Verified by execution.
Elimination
Eliminate the wrong options
After 1 and 2 are printed, what is the final line?
3 of these 4 are wrong. Strike them one at a time, and say what rules each one out before you strike the next. The survivor is the answer.
Survives elimination: A
Why: print shows each value but returns None, and the comprehension collects those return values, so out is [None, None]. This is why a side-effect-only comprehension should be a loop. Verified by execution.
Check
print returns nothing useful - what gets collected?
nums = [1, 2]
out = [print(x) for x in nums]
print(out)| x | print(x) shows | print(x) returns |
|---|---|---|
| 1 | 1 | ? |
| 2 | 2 | ? |
Check your understanding
After 1 and 2 are printed, what is the final line?
Answer: A
Why: print shows each value but returns None, and the comprehension collects those return values, so out is [None, None]. This is why a side-effect-only comprehension should be a loop. Verified by execution.
Trade off
Comparison matrix
From Check: side effect result: every row here is a choice with a cost. Fill the print(x) shows column, then say which row you would actually pick and what you give up for it.
| x | print(x) shows | print(x) returns |
|---|---|---|
| 1 | 1 | ? |
| 2 | 2 | ? |
Check
Left for is the outer loop.
grid = [[1, 2, 3], [4, 5, 6]]
flat = [n for row in grid for n in row]
print(flat)| row | n values |
|---|---|
| [1, 2, 3] | 1, 2, 3 |
| [4, 5, 6] | 4, 5, 6 |
Check your understanding
What does this print?
Answer: A
Why: The outer loop picks each row, the inner loop yields each number, so the rows are flattened in order into [1, 2, 3, 4, 5, 6]. Verified by execution.
Comparison
Comparison matrix
From Check: flatten with nested iteration: refill the n values column from what you know. The rest of the table is as it appeared.
| row | n values |
|---|---|
| [1, 2, 3] | 1, 2, 3 |
| [4, 5, 6] | 4, 5, 6 |
Connect it up
Draw it
One page, no notation unless you need it: draw how these connect — Building a List in One Line · From Loop to Comprehension · Filtering with if · Dict Comprehensions · Set Comprehensions · Nested Iteration. Put an arrow wherever one of them is what makes another possible, and label the arrow with why.
Recap
A comprehension builds a collection in one line: [expr for x in it], add if cond to filter, and swap the brackets for a dict or set.
| You write | It builds |
|---|---|
| [n * n for n in range(5)] | a list: [0, 1, 4, 9, 16] |
| [n for n in nums if n > 0] | a filtered list |
| {name: len(name) for name in names} | a dict of key: value |
| {n * n for n in nums} | a set (duplicates dropped) |
| [n for row in grid for n in row] | a flattened list (nested) |
Reach for a comprehension to build a collection; keep a plain loop for side effects, busy bodies, or when you need the variable afterward. The comprehension variable stays private - it never leaks.
Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.