Session 17 - Comprehensions

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

What this lesson covers

The lesson, slide by slide

1. Comprehensions

Title

Python Fundamentals - Session 17

Build a whole list, dict, or set in one readable line

2. What you will be able to do

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:

  1. Write a list comprehension [expr for x in it] and add a filter with if.
  2. Rewrite an accumulate-in-a-loop block as a comprehension.
  3. Build a dict with {k: v for ...} and a set with {expr for ...}.
  1. Loop over two things at once with nested iteration.
  2. Decide when a plain loop reads better than a comprehension.
  3. Avoid the traps: the leaked variable, the side-effect list, the over-nested one-liner.

3. What survived from Session 16 - Sets & Set Operations?

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 ^.

4. Building a List in One Line

Section

Part 1

5. A comprehension builds a list from another sequence

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.

6. Break it if you can: A comprehension builds a list from another…

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.

7. Read it as expression, then for

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.

8. By analogy: Read it as expression, then for

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.

9. What has to happen first: Squares of 0 to 4

Ranking

Put in order

Put the moves of Squares of 0 to 4 into the order they have to happen.

  1. The loop walks n over 0,1,2,3,4
  2. Each n contributes n * n to the new list
  3. Read the output

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.

10. Squares of 0 to 4

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].

nn * nlist so far
00[0]
11[0, 1]
24[0, 1, 4]
39[0, 1, 4, 9]
416[0, 1, 4, 9, 16]

11. Fill in: n * n for Squares of 0 to 4

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.

nn * nlist so far
00[0]
11[0, 1]
24[0, 1, 4]
39[0, 1, 4, 9]
416[0, 1, 4, 9, 16]

12. Predict the next row: Double every number

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]

nn * 2list so far
12[2]
24[2, 4]
36[2, 4, 6]
48[2, 4, 6, 8]
510[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.

13. Double every number

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].

nn * 2list so far
12[2]
24[2, 4]
36[2, 4, 6]
48[2, 4, 6, 8]
510[2, 4, 6, 8, 10]

14. What each one costs: Double every number

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.

nn * 2list so far
12[2]
24[2, 4]
36[2, 4, 6]
48[2, 4, 6, 8]
510[2, 4, 6, 8, 10]

15. The expression can be anything

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.

16. Teach it back: The expression can be anything

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.

17. Restore the missing line: Length of each word

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.

18. Length of each word

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].

wlen(w)list so far
"hi"2[2]
"there"5[2, 5]
"you"3[2, 5, 3]

19. Watch it run: Length of each word

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?

  1. Step 1: w is "hi"
  2. Step 2: w is "there"
  3. Step 3: w is "you"

20. The first letter of each name

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'].

namename[0]initials
"ann"'a'['a']
"bob"'b'['a', 'b']
"cid"'c'['a', 'b', 'c']

21. Draw the shape of it: The first letter of each name

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.

22. From Loop to Comprehension

Section

Part 2

23. The accumulate-in-a-loop pattern

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.

24. The loop version first

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.

nappendsquares
00[0]
11[0, 1]
24[0, 1, 4]
39[0, 1, 4, 9]
416[0, 1, 4, 9, 16]

25. Inspect it line by line: The loop version first

Error analysis

Annotate

Walk the callouts on The loop version first. Each one is a place this is easy to get subtly wrong.

  • squares starts empty; each pass appends n * n.
  • Verified by execution: [0, 1, 4, 9, 16] - the same result as the comprehension.

26. Not every loop should convert

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.

27. Fold three lines into one

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.

28. Add a fee to every price (loop, then comprehension)

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.

pp + 3with_fee
1013[13]
2528[13, 28]
58[13, 28, 8]
4043[13, 28, 8, 43]

29. Filtering with if

Section

Part 3

30. Add if to keep only some items

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.

31. Take the definitions apart: list comprehension vs filter clause

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.

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.
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.
b1
A compact expression that builds a list: [expression for item in iterable]. It runs the expression once per item and collects the results.
b2
The optional if cond at the end of a comprehension. When it is false the item is skipped entirely - nothing is added for it.

32. Keep the even numbers

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].

nn % 2 == 0kept?evens
4Trueyes[4]
7Falseno[4]
10Trueyes[4, 10]
3Falseno[4, 10]
8Trueyes[4, 10, 8]

33. What happens as it grows: Keep the even numbers

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?

  1. Step 1: n is 4
  2. Step 2: n is 7
  3. Step 3: n is 10
  4. Step 4: n is 3
  5. Step 5: n is 8

34. Only the passing scores

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].

ss >= 60passing
55False[]
82True[82]
90True[82, 90]
47False[82, 90]
73True[82, 90, 73]

35. Where does each piece belong: Session 17 - Comprehensions

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.

Building a List in One Line
A comprehension builds a list from another sequence; Read it as expression, then for; Squares of 0 to 4
From Loop to Comprehension
The accumulate-in-a-loop pattern; The loop version first; Not every loop should convert
Filtering with if
Add if to keep only some items; Keep the even numbers; Only the passing scores
s1
Building a List in One Line is where Session 17 - Comprehensions puts A comprehension builds a list from another sequence, Read it as expression, then for, Squares of 0 to 4. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s2
From Loop to Comprehension is where Session 17 - Comprehensions puts The accumulate-in-a-loop pattern, The loop version first, Not every loop should convert. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.
s3
Filtering with if is where Session 17 - Comprehensions puts Add if to keep only some items, Keep the even numbers, Only the passing scores. Knowing which part of the lesson a problem belongs to is most of knowing which method to reach for.

36. Filter and transform together

Concept

The expression at the front and the if at the back are independent. You can reshape the survivors while dropping the rest.

37. Square only the even numbers

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].

neven?n * neven_squares
1no-[]
2yes4[4]
3no-[4]
4yes16[4, 16]
6yes36[4, 16, 36]

38. Watch it run: Square only the even numbers

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?

  1. Step 1: n is 1
  2. Step 2: n is 2
  3. Step 3: n is 3
  4. Step 4: n is 4
  5. Step 5: n is 6

39. A filter can keep nothing

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: [].

nn % 2 == 0evens
1False[]
3False[]
5False[]

40. Dict Comprehensions

Section

Part 4

41. Build a dict with key: value

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.

42. Map each name to its length

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}.

namekeyvaluedict so far
"ann"'ann'3{'ann': 3}
"bob"'bob'3{'ann': 3, 'bob': 3}
"cid"'cid'3{'ann': 3, 'bob': 3, 'cid': 3}

43. Transform an existing dict with .items()

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}.

itemcostcost * 2doubled
"pen"24{'pen': 4}
"pad"510{'pen': 4, 'pad': 10}
"ink"816{'pen': 4, 'pad': 10, 'ink': 16}

44. Restore the missing line: A number-to-square lookup

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.

45. A number-to-square lookup

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}.

nkeyvaluesquares
111{1: 1}
224{1: 1, 2: 4}
339{1: 1, 2: 4, 3: 9}
4416{1: 1, 2: 4, 3: 9, 4: 16}

46. Filter a dict comprehension

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}.

namescsc >= 60kept?
"ann"90Trueyes
"bob"45Falseno
"cid"72Trueyes

47. Duplicate keys: the last one wins

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.

wlen(w)by_len
"ann"3{3: 'ann'}
"bob"3{3: 'bob'}
"cid"3{3: 'cid'}
"dan"3{3: 'dan'}

48. Set Comprehensions

Section

Part 5

49. Curly braces, no colon, builds a set

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.

50. Term to definition: Session 17 - Comprehensions

Matching

Match the pairs

Match each term to the definition this lesson gave it — not the one you would guess from the word.

  • t1. list comprehension
  • t2. filter clause
  • t3. dict comprehension
  • t4. set comprehension
  • d1. A compact expression that builds a list: [expression for item in iterable]. It runs the expression once per item and collects the results.
  • d2. The optional if cond at the end of a comprehension. When it is false the item is skipped entirely - nothing is added for it.
  • d3. Builds a dictionary: {key: value for item in iterable}. The colon separates the key expression from the value expression.
  • d4. Builds a set: {expression for item in iterable}. Like a list comprehension but the result keeps only distinct values.

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.

51. Distinct squares

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}.

nn * nset so far
11{1}
24{1, 4}
24{1, 4}
39{1, 4, 9}
39{1, 4, 9}

52. Fill in: set so far for Distinct squares

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.

nn * nset so far
11{1}
24{1, 4}
24{1, 4}
39{1, 4, 9}
39{1, 4, 9}

53. The distinct word lengths

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}.

wlen(w)sizes
"a"1{1}
"bb"2{1, 2}
"cc"2{1, 2}
"ddd"3{1, 2, 3}

54. Which is which, by len(w)

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.

1
"a"
2
"bb"; "cc"
3
"ddd"
g1
len(w) is "1" for "a" — that is what the table on "The distinct word lengths" records, and it is the single property separating this group from the rest.
g2
len(w) is "2" for "bb", "cc" — that is what the table on "The distinct word lengths" records, and it is the single property separating this group from the rest.
g3
len(w) is "3" for "ddd" — that is what the table on "The distinct word lengths" records, and it is the single property separating this group from the rest.

55. Same brackets, different result

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.

56. Nested Iteration

Section

Part 6

57. Two for clauses run left to right

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.

58. Every pair of two lists

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)].

xypairpairs so far
110(1, 10)[(1, 10)]
120(1, 20)[(1, 10), (1, 20)]
210(2, 10)[(1, 10), (1, 20), (2, 10)]
220(2, 20)[(1, 10), (1, 20), (2, 10), (2, 20)]

59. Flatten a grid of rows

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].

rownflat 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]

60. Predict the next row: Flatten and filter at once

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]

rowneven?flat_evens
[1, 2]1no[]
[1, 2]2yes[2]
[3, 4]4yes[2, 4]
[5, 6]6yes[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.

61. Flatten and filter at once

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].

rowneven?flat_evens
[1, 2]1no[]
[1, 2]2yes[2]
[3, 4]4yes[2, 4]
[5, 6]6yes[2, 4, 6]

62. Watch it run: Flatten and filter at once

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?

  1. Step 1: row is [1, 2]
  2. Step 2: row is [1, 2]
  3. Step 3: row is [3, 4]
  4. Step 4: row is [5, 6]

63. When a Loop is Clearer

Section

Part 7

64. Comprehensions are for building collections

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.

65. Teach it back: Comprehensions are for building collections

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.

66. A short choice fits in a comprehension

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'].

pp >= 20labellabels
10Falsecheap['cheap']
25Trueexpensive['cheap', 'expensive']
5Falsecheap['cheap', 'expensive', 'cheap']
40Trueexpensive['cheap', 'expensive', 'cheap', 'expensive']

67. If you cannot read it, write the loop

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.

68. By analogy: If you cannot read it, write the loop

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.

69. Restore the missing line: The same labels, spelled out as a loop

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.

70. The same labels, spelled out as a loop

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'].

pbranchlabels
10else['cheap']
25if['cheap', 'expensive']
5else['cheap', 'expensive', 'cheap']
40if['cheap', 'expensive', 'cheap', 'expensive']

71. Draw the shape of it: The same labels, spelled out as a loop

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.

72. Traps

Section

Part 8

73. Something is wrong here: the comprehension variable does not leak

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.

74. Trap: the comprehension variable does not leak

Trap

The 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.

lineresult
print(squares)[0, 1, 4, 9, 16]
print(n)NameError: name 'n' is not defined

The fix

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.

constructis n defined after?
comprehensionno (NameError)
for loopyes (holds last value 4)

75. Inspect it line by line: Trap: the comprehension variable does not…

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.

  • The comprehension has its own private scope, so n is gone the moment it finishes - the last print crashes.
  • 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.

76. That private scope is a feature

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'.

77. Break it if you can: That private scope is a feature

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.

78. Something is wrong here: a comprehension only for side effects

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.

79. Trap: a comprehension only for side effects

Trap

The 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.

stepoutput / value
name = "ann"prints ann, collects None
name = "bob"prints bob, collects None
result[None, None]

The fix

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.

approachscreenbuilds a list?
comprehensionann / bobyes - [None, None] wasted
for loopann / bobno

80. Where the cost goes: Trap: a comprehension only for side effects

Cost model

Annotate

In Trap: a comprehension only for side effects, before reading the notes: mark where the time actually goes. Which line dominates?

  • It prints ann then bob, but also collects each print's return value. result is a useless list of None that wasted memory.
  • 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.

81. Something is wrong here: an over-nested one-liner

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.

82. Trap: an over-nested one-liner

Trap

The 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.

xyx != ykept
12True2
13True3
21True2
23True6
31True3

The fix

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.

formoutputreadable?
nested one-liner[2, 3, 2, 6, 3, 6]hard
nested for loops[2, 3, 2, 6, 3, 6]easy

83. Which of these survive contact with Session 17 - Comprehensions?

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.

Holds up
A list comprehension walks a sequence and collects one new value per item - all inside a pair of square brackets.; The shape is [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.
Breaks
You expect the loop variable to survive, like it does after a for loop.; Using a comprehension just to print - throwing the result away.
sound
These are stated as this lesson states them — each one survives the edge cases Session 17 - Comprehensions puts it through.
flawed
Each of these is lifted from a trap in this deck: reasonable-sounding, and wrong in a way that only shows up once you rely on it.

84. Patterns & Checks

Section

Part 9

85. Writing a comprehension

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.

86. Comprehension or loop?

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.

87. Where this shows up: Session 17 - Comprehensions

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.

88. Check: basic list comprehension

Check

Predict the list before clicking.

words = ["cat", "dog", "bird"]
caps = [w.upper() for w in words]
print(caps)
ww.upper()
"cat"?
"dog"?
"bird"?

Check your understanding

What does this print?

  • A. ['CAT', 'DOG', 'BIRD'] (correct)
  • B. ['cat', 'dog', 'bird']
  • C. 'CATDOGBIRD'
  • D. [3, 3, 4]

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.

Why B tempts people
w.upper() changes each word to uppercase; the originals are not kept.
Why C tempts people
A comprehension builds a list, not one joined string - each result is a separate element.
Why D tempts people
That would be [len(w) for w in words]; the expression here is .upper(), not len().

89. Check: filter clause

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)
nn > 2n + 1 (if kept)
1False-
2False-
3True?
4True?

Check your understanding

What does this print?

  • A. [4, 5] (correct)
  • B. [3, 4]
  • C. [2, 3, 4, 5]
  • D. [4, 5, 2, 3]

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.

Why B tempts people
These are the surviving inputs (3 and 4) before the + 1 expression is applied.
Why C tempts people
The filter n > 2 removes 1 and 2, so not every item is transformed.
Why D tempts people
Filtered-out items are skipped, not added on afterward - the result stays in original order.

90. What happens as it grows: Check: filter clause

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?

  1. Step 1: n is 1
  2. Step 2: n is 2
  3. Step 3: n is 3
  4. Step 4: n is 4

91. Check: dict comprehension

Check

Watch the colon - key on the left, value on the right.

d = {x: x * 10 for x in [1, 2, 3]}
print(d)
xkeyvalue
11?
22?
33?

Check your understanding

What does this print?

  • A. {1: 10, 2: 20, 3: 30} (correct)
  • B. [10, 20, 30]
  • C. {10, 20, 30}
  • D. {1: 1, 2: 2, 3: 3}

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.

Why B tempts people
Curly braces with a colon build a dict, not a list; and the keys are kept too.
Why C tempts people
That is a set of the values - but the colon here means key: value, so it is a dict.
Why D tempts people
The value expression is x * 10, not x, so the values are 10, 20, 30.

92. What stays fixed: Check: dict comprehension

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.

  1. Step 1: x is 1
  2. Step 2: x is 2
  3. Step 3: x is 3

93. Check: set comprehension

Check

Two of these words share a length.

words = ["a", "bb", "cc", "ddd"]
sizes = {len(w) for w in words}
print(sizes)
wlen(w)
"a"1
"bb"2
"cc"2
"ddd"3

Check your understanding

What does this print?

  • A. {1, 2, 3} (correct)
  • B. {1, 2, 2, 3}
  • C. [1, 2, 2, 3]
  • D. {'a': 1, 'bb': 2, 'cc': 2, 'ddd': 3}

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.

Why B tempts people
A set cannot hold the same value twice - the repeated 2 is stored only once.
Why C tempts people
Curly braces with a bare expression build a set, not a list.
Why D tempts people
With no colon this is a set of lengths, not a dict mapping words to lengths.

94. Which is which, by len(w)

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.

1
"a"
2
"bb"; "cc"
3
"ddd"
g1
len(w) is "1" for "a" — that is what the table on "Check: set comprehension" records, and it is the single property separating this group from the rest.
g2
len(w) is "2" for "bb", "cc" — that is what the table on "Check: set comprehension" records, and it is the single property separating this group from the rest.
g3
len(w) is "3" for "ddd" — that is what the table on "Check: set comprehension" records, and it is the single property separating this group from the rest.

95. Check: the variable's scope

Check

Think about where n lives.

squares = [n * n for n in range(3)]
print(n)
after the comprehensionis n defined?
print(n)?

Check your understanding

What happens on the last line?

  • A. NameError: name 'n' is not defined (correct)
  • B. It prints 2
  • C. It prints 4
  • D. It prints [0, 1, 4]

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.

Why B tempts people
That would be the last value if n leaked like a for-loop variable - but comprehension variables do not leak.
Why C tempts people
4 is the last square, but n itself is undefined afterward, so nothing prints.
Why D tempts people
The list is stored in squares, not in n; n is out of scope entirely.

96. Rule out three: Check: side effect result

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.

  • A. [None, None]
  • B. [1, 2]
  • C. None
  • D. []

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.

97. Check: side effect result

Check

print returns nothing useful - what gets collected?

nums = [1, 2]
out = [print(x) for x in nums]
print(out)
xprint(x) showsprint(x) returns
11?
22?

Check your understanding

After 1 and 2 are printed, what is the final line?

  • A. [None, None] (correct)
  • B. [1, 2]
  • C. None
  • D. []

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.

Why B tempts people
The comprehension collects what print RETURNS (None), not the values passed to it.
Why C tempts people
out is a list built by the comprehension, not a single None - it has one entry per item.
Why D tempts people
The comprehension runs twice and appends a result each time, so the list is not empty.

98. What each one costs: Check: side effect result

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.

xprint(x) showsprint(x) returns
11?
22?

99. Check: flatten with nested iteration

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)
rown values
[1, 2, 3]1, 2, 3
[4, 5, 6]4, 5, 6

Check your understanding

What does this print?

  • A. [1, 2, 3, 4, 5, 6] (correct)
  • B. [[1, 2, 3], [4, 5, 6]]
  • C. [1, 4, 2, 5, 3, 6]
  • D. [6, 5, 4, 3, 2, 1]

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.

Why B tempts people
Flattening removes the inner brackets - the result is one flat list, not a list of lists.
Why C tempts people
The loops are not interleaved column by column; the whole first row comes before the second.
Why D tempts people
Nothing reverses the order - items are collected exactly as the loops visit them.

100. Fill in: n values for Check: flatten with nested iteration

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.

rown values
[1, 2, 3]1, 2, 3
[4, 5, 6]4, 5, 6

101. Connect it up: Session 17 - Comprehensions

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.

102. What you can do now

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 writeIt 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.

Sources

  1. Python 3 Tutorial - List Comprehensions
  2. Python 3 Tutorial - Nested List Comprehensions
  3. Python 3 Reference - Displays for lists, sets and dictionaries
  4. All snippets and error messages executed and copied from CPython 3.12. — Author verification run, 2026-07-15 (Python Fundamentals series, Session 17).

Want this taught 1-on-1? Alexander tutors Python Fundamentals — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108