Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 5 – Built-in data structures: lists, dicts, sets and more

List, dict and set comprehensions

Build lists, dictionaries and sets in one expression with Python comprehensions: filters, conditional values, nested loops, scope and when a loop reads better.

  • Beginner
  • 25 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free lesson, Pro answers: 25 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Write list, dict and set comprehensions with a filter or a conditional value
  • Predict the order and shape of the result of nested for clauses and nested comprehensions
  • Explain how a comprehension's variables are scoped, in a module, a function and a class body
  • Choose a generator expression when the values are used only once
  • Decide when a plain for loop is clearer than a comprehension

Before you start

What you’ll learn · Before you start

What you will learn

  • Write list, dict and set comprehensions with a filter or a conditional value
  • Predict the order and shape of the result of nested for clauses and nested comprehensions
  • Explain how a comprehension's variables are scoped, in a module, a function and a class body
  • Choose a generator expression when the values are used only once
  • Decide when a plain for loop is clearer than a comprehension

Before you start

On this page

A loop that keeps the passing marks of a class takes four lines: an empty list, a for, an if and an append(). A comprehension: An expression in square brackets or braces that builds a new list, dict or set from the items of an iterable, such as [m for m in marks if m >= 40]. It runs a loop of its own and never changes what it reads. does the same job in one expression, [m for m in marks if m >= 40], which reads almost like the sentence it stands for: each mark, for every mark in marks, if that mark is at least 40. It never changes the list it reads, and there is a form for each built-in container: square brackets for a list, braces with key: value for a dictionary, and braces with a single expression for a set.

Below you can run seven marks through a comprehension one at a time, watch a dict comprehension lose a student, step through the two loops that two for clauses stand for, see which names a comprehension can reach, and use up a generator expression. The last section shows where a comprehension stops being clearer than the loop it replaces.

A comprehension is a loop that builds a new list

Here is a list of marks filtered twice, once with a loop and once with a comprehension, followed by a comprehension that changes every item and one that does both:

The same filter, written twice Python · loop_vs_comprehension.py
marks = [72, 35, 88, 41, 29, 95, 60]

# The loop: start empty, test each mark, append the ones that pass.
passed_loop = []
for m in marks:
    if m >= 40:
        passed_loop.append(m)

# The comprehension: the same three parts in one expression.
passed = [m for m in marks if m >= 40]

print(passed_loop)
print(passed)
print(passed == passed_loop)

# Without an if clause, every item makes it into the new list.
with_grace = [m + 5 for m in marks]
print(with_grace)

# Both clauses: keep the passing marks, then add 5 to each of them.
print([m + 5 for m in marks if m >= 40])
print(marks)  # the original list is unchanged

Output

[72, 88, 41, 95, 60]
[72, 88, 41, 95, 60]
True
[77, 40, 93, 46, 34, 100, 65]
[77, 93, 46, 100, 65]
[72, 35, 88, 41, 29, 95, 60]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 loop_vs_comprehension.py

The comprehension has the same three parts as the loop, in a different order:

  • The for clause, for m in marks, is the header of the loop without its colon. It names each item in turn. Any iterable: A value a for loop can go through one item at a time, such as a string, a list, a tuple, a dict, a set or a range. A dict gives its keys. More in “for loops with range, enumerate and zip” works here: a list, a range(), a string, a dictionary or the lines of a file.
  • The if clause, if m >= 40, is optional. An item is added only when the condition is true.
  • The expression before for, here plain m, says what to put in the new list for each item that was kept. In [m + 5 for m in marks] it is m + 5, and without an if clause every item is used.

The expression is written first, so a comprehension reads as “what you get, then where it comes from”, but it runs last for each item. Step through the comprehension that does both jobs, [m + 5 for m in marks if m >= 40], on the same seven marks:

Interactive · stage by stage [m + 5 for m in marks if m >= 40], mark by mark Step through the seven marks. Each one goes down the column, the for clause first, and the sub-labels show what each part did with it.
[m + 5 for m in marks if m >= 40], mark by mark, step 9 of 9. Showing for m in marks (no marks left), if m >= 40 (60 >= 40: yes), m + 5 (65), append to the new list ([77, 93, 46, 100, 65]).nonext markfor m in marksno marks leftif m >= 4060 >= 40: yesm + 565append to the new list[77, 93, 46, 100, 65]

The drawing shows the whole scene; its 9 steps are listed below.

Every step as text
  1. Four parts, in this order. The expression m + 5 is written first, but it runs third. For each mark the for clause takes it, the if clause tests it, the expression works out the item, and the item is added to a new list.
  2. 72 is kept. for m in marks takes 72. 72 >= 40 is True, so m + 5 gives 77, the first item of the new list.
  3. 35 is skipped. 35 >= 40 is False, so the expression never runs for 35 and nothing is added. The loop goes straight back to the for clause.
  4. 88 is kept. 88 passes the test, so 93 joins the new list.
  5. 41 is kept, just. 41 >= 40 is True, by one mark, so 46 joins the list.
  6. 29 is skipped. 29 fails the test, so the expression does not run and the list stays as it was.
  7. 95 is kept. 95 passes, and m + 5 gives 100.
  8. 60 is kept. 60 passes too, and 65 becomes the last item.
  9. No marks left. The for clause finds no mark after 60, so the loop ends, and the new list [77, 93, 46, 100, 65] is the value of the whole comprehension, as the program printed. marks itself never changed.

35 and 29 never reached the expression, and the other five came out 5 marks higher, in their original order: [77, 93, 46, 100, 65], the fifth line of the output. The last line matters as much as the others: marks still holds the original numbers. A comprehension always builds a new list, which you keep by assigning it to a name.

Key idea

A comprehension runs a loop of its own: for each item it takes, it tests the if clause and then works out the expression, and the results, in order, make a new list.

Checkpoint

Check: A comprehension builds a new list

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    marks = [72, 35, 88]
    print([m + 5 for m in marks if m >= 40], marks)
    Show the answer: Question 1 of 2

    Answer: [77, 93] [72, 35, 88]

    The for clause takes each mark, the if clause keeps 72 and 88, and the expression adds 5 to each of them: [77, 93]. marks still holds its three original numbers.

  2. Question 2 of 2 In [m + 5 for m in marks if m >= 40], which part runs last for each mark that is kept?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: m + 5, which works out the item to add

    Read a comprehension as "what you get, then where it comes from", but run it as a loop: take, test, work out, add.

if after for filters; if-else before for picks a value

An if can appear in two places, and the place decides what it does. After the for clause it filters: items that fail the test are left out, so the new list can be shorter. Before the for clause, as a if condition else b, it is a conditional expression: An expression of the form a if condition else b. It evaluates the condition and gives a when the condition is true and b otherwise, evaluating only the side it picks. More in “Booleans, None, truthiness and comparisons” that chooses a value for every item, so the new list always has the same length as the input. The readings below come from a sensor that writes -999 when a measurement fails:

Leave failed readings out, or mark them Python · filter_or_choose.py
readings = [21.5, -999, 23.0, 22.4, -999, 24.1]  # -999 marks a failed reading

# if after the for clause filters: failed readings are left out.
valid = [r for r in readings if r != -999]

# if ... else before the for clause chooses a value: every reading stays.
filled = [r if r != -999 else None for r in readings]

print(len(readings), "readings")
print(len(valid), "valid:", valid)
print(len(filled), "filled:", filled)
print("average of the valid ones:", round(sum(valid) / len(valid), 2))

Output

6 readings
4 valid: [21.5, 23.0, 22.4, 24.1]
6 filled: [21.5, None, 23.0, 22.4, None, 24.1]
average of the valid ones: 22.75

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 filter_or_choose.py

Use the filter when the bad items should disappear, as for the average, which must not count -999 as a temperature. Use the conditional expression when positions matter, for example when reading number 2 must stay reading number 2 in a chart, with None in place of the failed value. Press Edit, change the filter’s test to r > 22 and run it: three readings are left, and the average of the valid ones becomes 23.17.

The table sets the two forms side by side, with the six readings of the program:

if after for, or if-else before it?
Criterion FilterChoose a value
Where the if goes after forbefore for, with else
A failed reading left outbecomes None
Length of the result 4 of 6: it can shrinkalways 6
Good for the averagea chart by position
When to choose Bad items should goPositions must stay

Mixing the two forms up gives a syntax error. An else after the for clause is not part of any expression, so Python stops before it runs a single line:

else belongs before the for Python · else_after_for.py
readings = [21.5, -999, 23.0]

# compile() turns text into code without running it, so this program can
# catch the SyntaxError and print it instead of stopping.
wrong = "[r for r in readings if r != -999 else 0]"
try:
    compile(wrong, "<attempt>", "eval")
except SyntaxError as err:
    print(f"{wrong}\nSyntaxError: {err.msg}")

# The else belongs to a conditional expression, which comes before "for":
print([r if r != -999 else 0 for r in readings])

Output

[r for r in readings if r != -999 else 0]
SyntaxError: invalid syntax
[21.5, 0, 23.0]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 else_after_for.py

Python 3.14 reports only invalid syntax, and when the line is in a script, the traceback’s markers point at the else. The fix is to move the whole if ... else in front of for.

Checkpoint

Check: Filter, or choose a value

  1. Question 1 of 2 readings has 6 items, 2 of which are -999. How many items does [r if r != -999 else None for r in readings] have?

    Type a number.

    A hint

    Where is the if, before or after the for clause?

    Show the answer: Question 1 of 2

    Answer: 6 items

    The if ... else comes before the for clause, so it is a conditional expression that chooses a value for every reading: the two failed readings become None, and the list keeps all 6 positions.

  2. Question 2 of 2 Which comprehension replaces each negative number in xs by 0 and keeps the others as they are?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: [x if x >= 0 else 0 for x in xs]

    To replace values, choose with a conditional expression before for. To drop values, filter with an if after the for clause.

Braces make dict and set comprehensions

Braces make the other two kinds. A dict comprehension has a key: value pair where the list form has its expression, and a set comprehension has a single expression:

Marks to grades Python · grades.py
marks = {"Asha": 91, "Ravi": 67, "Meera": 78, "John": 45, "Divya": 88}


def grade(mark):
    if mark >= 85:
        return "A"
    if mark >= 70:
        return "B"
    if mark >= 50:
        return "C"
    return "D"


# Dict comprehension: a key: value pair for every item.
grades = {name: grade(mark) for name, mark in marks.items()}
print(grades)

# Set comprehension: each grade once, however many students have it.
given = {g for g in grades.values()}
print(sorted(given))

# Swapping keys and values: a key can appear only once,
# so when two students share a grade, the later one wins.
by_grade = {g: name for name, g in grades.items()}
print(by_grade)

# To keep everyone, build a list for each grade instead.
everyone = {g: [n for n, x in grades.items() if x == g] for g in "ABCD"}
print(everyone)

Output

{'Asha': 'A', 'Ravi': 'C', 'Meera': 'B', 'John': 'D', 'Divya': 'A'}
['A', 'B', 'C', 'D']
{'A': 'Divya', 'C': 'Ravi', 'B': 'Meera', 'D': 'John'}
{'A': ['Asha', 'Divya'], 'B': ['Meera'], 'C': ['Ravi'], 'D': ['John']}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 grades.py

  • {name: grade(mark) for name, mark in marks.items()} builds the grade book. The for clause unpacks each pair from items() into two names, exactly as a for loop over items() does.
  • {g for g in grades.values()} keeps each grade once: a set holds no duplicates. Sets have no fixed order, so the program sorts the grades before printing them, which keeps the output the same from run to run.

Swapping keys and values loses data whenever two keys share a value. A dictionary holds each key once, and when a comprehension produces the same key again, the later value replaces the earlier one. Step through by_grade to see who is lost:

Interactive · step by step Swapping keys and values loses a student Step through the dict comprehension. Each pair of grades.items() makes its grade a key, with the student's name as the value.
Swapping keys and values loses a student, step 6 of 6. Showing Asha, 'A', Ravi, 'C', Meera, 'B', John, 'D', Divya, 'A', key 'A' (value 'Divya'), key 'C' (value 'Ravi'), key 'B' (value 'Meera'), key 'D' (value 'John').same keyAsha, 'A'Ravi, 'C'Meera, 'B'John, 'D'Divya, 'A'key 'A'value 'Divya'key 'C'value 'Ravi'key 'B'value 'Meera'key 'D'value 'John'

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One pair at a time. The comprehension reads the pairs of grades.items() in order. For each name and grade it adds the entry g: name, so the grade becomes the key.
  2. Asha. Asha has an 'A', so the dictionary gets the key 'A' with the value 'Asha'.
  3. Ravi and Meera. Ravi's 'C' and Meera's 'B' are new keys, so each becomes an entry of its own.
  4. John. John's 'D' is new as well: four keys so far.
  5. A key comes back. Divya also has an 'A'. A dictionary holds each key once, so the comprehension replaces the value of 'A': 'Divya' wins, and Asha is lost without any error.
  6. Five students, four keys. by_grade ends as {'A': 'Divya', 'C': 'Ravi', 'B': 'Meera', 'D': 'John'}, as the program printed. To keep everyone, the program's last comprehension builds a list of names for each grade.

Asha’s "A" was replaced by Divya’s, and nothing warned about it. To keep everyone, make the value a list, as the last comprehension of the program does with a comprehension inside it.

Empty braces {} are an empty dictionary, never an empty set; set() makes an empty set. A set comprehension with no items to add, such as {x for x in []}, also gives set().

Checkpoint

Check: Dict and set comprehensions

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    words = ["ox", "cat", "be", "dog"]
    print(sorted({len(w) for w in words}))
    Show the answer: Question 1 of 2

    Answer: [2, 3]

    Braces with a single expression make a set, so the four lengths 2, 3, 2 and 3 become the set {2, 3}; sorting turns it into the list [2, 3].

  2. Question 2 of 2 What is {w[0]: w for w in ["ant", "bee", "ape"]}?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: {'a': 'ape', 'b': 'bee'}

    A dictionary holds each key once. The comprehension sets the key 'a' for "ant" and again for "ape", so "ant" is lost.

Two for clauses nest like loops; an inner comprehension builds rows

A comprehension may have several for clauses, and if clauses between them. They nest from left to right, just as the loops would be written one inside the other, and the result is one flat list. Step through the two loops that [m for row in marks for m in row] stands for, on the first two rows of a table of marks:

Interactive · line by line Two loops, one flat list Step through the loops. The flag under marks is the row the outer loop reads next; the flag under row is the mark the inner loop reads next.

Program · flatten_steps.py

marks = [[78, 64, 90], [55, 81, 70]]
flat = []
for row in marks:
    for m in row:
        flat.append(m)
print(flat)

Variables

After line 6: marks is [[78, 64, 90], [55, 81, 70]]; flat is [78, 64, 90, 55, 81, 70]; row is [55, 81, 70]; m is 70.marks[78, 64, 90]0[55, 81, 70]1flat780641902553814705row550811702m70
  • just taken by the loop
  • changed in this step
  • passed by the loop
  • read next
  • where the loop stopped

Printed so far

[78, 64, 90, 55, 81, 70]

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 21 steps are listed below.

Line run 6
Next line end
Loop pass –
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. marks = [[78, 64, 90], [55, 81, 70]] makes marks a list of 2 items.
  3. Line 2. flat = [] makes flat a list of 0 items.
  4. Line 3. for row in marks: takes the item at position 0 of marks: row is [78, 64, 90].
  5. Line 4. for m in row: takes the item at position 0 of row: m is 78. The inner loop reads row, the list the outer loop has just taken. It starts again from position 0 for every row.
  6. Line 5. flat.append(m) adds 78 to the end of flat.
  7. Line 4. for m in row: takes the item at position 1 of row: m is 64.
  8. Line 5. flat.append(m) adds 64 to the end of flat.
  9. Line 4. for m in row: takes the item at position 2 of row: m is 90.
  10. Line 5. flat.append(m) adds 90 to the end of flat.
  11. Line 4. for m in row: finds no item at position 3 of row (it has 3 items), so the loop ends.
  12. Line 3. for row in marks: takes the item at position 1 of marks: row is [55, 81, 70]. Key point The outer loop moves on to the next row only after the inner loop has read every mark of the first one.
  13. Line 4. for m in row: takes the item at position 0 of row: m is 55.
  14. Line 5. flat.append(m) adds 55 to the end of flat.
  15. Line 4. for m in row: takes the item at position 1 of row: m is 81.
  16. Line 5. flat.append(m) adds 81 to the end of flat.
  17. Line 4. for m in row: takes the item at position 2 of row: m is 70.
  18. Line 5. flat.append(m) adds 70 to the end of flat.
  19. Line 4. for m in row: finds no item at position 3 of row (it has 3 items), so the loop ends.
  20. Line 3. for row in marks: finds no item at position 2 of marks (it has 2 items), so the loop ends.
  21. Line 6. print(flat) prints [78, 64, 90, 55, 81, 70]. That was the last line, so the program ends. [m for row in marks for m in row] writes the same two for clauses in the same order, outer one first, and builds the same flat list.

The inner loop read all three marks of a row before the outer loop moved on to the next row, so flat holds the marks row by row. A comprehension can also sit inside the expression of another one, and then each inner comprehension builds a list of its own. The two look alike and do different things, as the full table shows, with one row per student and one column per subject:

Flatten, then turn rows into columns Python · transpose.py
# One row per student, one column per subject: maths, physics, chemistry.
marks = [
    [78, 64, 90],
    [55, 81, 70],
    [92, 88, 61],
    [40, 59, 75],
]

# Two for clauses, read left to right like two nested loops: one flat list.
flat = [m for row in marks for m in row]
print(flat)

# The same order with the loops written out:
flat_loop = []
for row in marks:
    for m in row:
        flat_loop.append(m)
print(flat == flat_loop)

# A comprehension inside the expression: one new list per column.
by_subject = [[row[col] for row in marks] for col in range(len(marks[0]))]
for subject, column in zip(["maths", "physics", "chemistry"], by_subject):
    print(f"{subject:9}", column, "best:", max(column))

# zip(*marks) pairs up the columns too, as tuples.
print(list(zip(*marks)))

Output

[78, 64, 90, 55, 81, 70, 92, 88, 61, 40, 59, 75]
True
maths     [78, 55, 92, 40] best: 92
physics   [64, 81, 88, 59] best: 88
chemistry [90, 70, 61, 75] best: 90
[(78, 55, 92, 40), (64, 81, 88, 59), (90, 70, 61, 75)]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 transpose.py

  • [m for row in marks for m in row] has two for clauses. Read them in the order they are written: for each row, for each mark in that row. The loop version below it produces the same list, which is a good way to check any comprehension you find hard to read.
  • [[row[col] for row in marks] for col in range(len(marks[0]))] has a comprehension as its expression. The outer one runs once per column, and each time the inner one collects that column from every row, so the result is a list of three lists.
  • zip(*marks) does the same transposition with a built-in: the * passes each row as a separate argument, and zip() pairs up their first items, then their second, and so on, giving tuples.

Version note

Python 3.15 accepts unpacking inside a comprehension, so [*row for row in grid] flattens a list of lists without a second for clause (PEP 798), and {**d for d in dicts} merges dictionaries. Python 3.14, which this track runs, rejects that form with a clear message:

Unpacking in a comprehension on Python 3.14 Python · unpacking_3_15.py
import sys

grid = [[1, 2], [3], [4, 5]]
source = "[*row for row in grid]"  # unpacking inside a comprehension (PEP 798)

print(f"Python {sys.version_info.major}.{sys.version_info.minor}")
try:
    print(eval(source))
except SyntaxError as err:
    print("SyntaxError:", err.msg)

# The two-clause form gives the same list on every version:
print([x for row in grid for x in row])

Output

Python 3.14
SyntaxError: iterable unpacking cannot be used in comprehension
[1, 2, 3, 4, 5]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unpacking_3_15.py

Checkpoint

Check: Nested for clauses and comprehensions

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    print([r + c for r in "ab" for c in "xy"])
    Show the answer: Question 1 of 2

    Answer: ['ax', 'ay', 'bx', 'by']

    [r + c for r in "ab" for c in "xy"] runs like two nested loops, for r, then for c inside it: 'ax', 'ay', 'bx', 'by'.

  2. Question 2 of 2 marks has 4 rows of 3 marks. What does [[row[col] for row in marks] for col in range(3)] make?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 3 lists of 4 marks each, one list per column

    The outer comprehension makes one item per col in range(3), and each item is the inner comprehension's list of that column's 4 marks: a 3 × 4 table, the transpose of the 4 × 3 one.

A comprehension’s own variables stay inside it

A comprehension runs in a scope: The region of a program in which a name is looked up. Python has four kinds: the local scope of a function, enclosing functions, the global scope of a module and the built-ins. More in “Scope: local, enclosing, global and built-in” of its own, so the names its for clauses assign never leak into the code around it. Two exceptions are worth knowing, and this program shows the rule and both of them:

What a comprehension can see and change Python · scope.py
x = "kept"
squares = [x * x for x in range(4)]
print(squares, x)  # the comprehension's x did not replace this one

# An assignment expression (:=) is the exception: it binds in the
# enclosing scope, so total is still there after the comprehension.
total = 0
running = [total := total + n for n in [5, 3, 8]]
print(running, total)


class Exam:
    max_marks = 50
    raw = [20, 35, 50]
    # The first iterable is evaluated in the class body, so raw is found.
    copied = [m for m in raw]
    # Everything else runs in the comprehension's own scope, which cannot
    # see the names of a class body.
    try:
        percent = [m * 100 // max_marks for m in raw]
    except NameError as err:
        print("NameError:", err)


print(Exam.copied)

Output

[0, 1, 4, 9] kept
[5, 8, 16] 16
NameError: name 'max_marks' is not defined
[20, 35, 50]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 scope.py

Step through the names of the program as its four comprehensions run. The boxes on the left belong to the module or to the class body, the round ones on the right to the comprehensions themselves:

Interactive · step by step Whose name is it? Step through the four comprehensions of the program. Boxes on the left are names of the module or the class body; boxes on the right are the comprehensions' own.
Whose name is it?, step 6 of 6. Showing x, total (module: 5, 8, then 16), n (5, then 3 and 8), class body of Exam, m. In focus: m, e-blind.total :=rawmax_marks?xmodule: 'kept'totalmodule: 5, 8, then 16n5, then 3 and 8class body of Exammax_marks and rawmcomprehension's ownNameError: max_marks

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. Two names called x. x = "kept" binds x in the module. The comprehension on the next line runs in a scope of its own, and its x is a different name.
  2. The inner x does the work. The comprehension's own x takes 0, 1, 2 and 3 in turn, and x * x gives [0, 1, 4, 9]. The module's x is not touched.
  3. The inner x is gone. When the comprehension ends, its scope goes and its x with it. The module's x still holds 'kept', as the first line of output shows.
  4. := reaches out. An assignment expression is the exception. total := total + n binds total in the scope around the comprehension, here the module, so the running total is still there afterwards, 16.
  5. The first iterable. In the class body, raw is the iterable of the first for clause of [m for m in raw]. That one part is evaluated in the class body itself, so raw is found and copied works.
  6. The class body is out of sight. Everything else runs in the comprehension's own scope, which cannot see names defined in a class body. So max_marks in m * 100 // max_marks is a NameError, as the third line of output shows.

  • The loop variable is private. The comprehension’s x went through 0 to 3, and the outer x still holds "kept" afterwards.
  • An assignment expression: name := value, nicknamed the walrus. It binds the value to the name and also gives the value back, so it can sit inside a larger expression such as a while condition. More in “Operators, precedence and the walrus” is not. total := total + n binds total in the enclosing scope (PEP 572 defines it that way on purpose), so a comprehension can keep a running total and leave the final value behind. Use it sparingly: a reader expects a comprehension to have no effect besides the value it returns.
  • A class body is not an enclosing scope. The iterable of the first for clause is evaluated where the comprehension is written, which is why [m for m in raw] found raw. Everything else runs in the comprehension’s own scope, and that scope cannot see names defined in a class body, so max_marks is a NameError. Compute such a list after the class, where you can write Exam.max_marks, or in a function that takes the value as an argument.

Before Python 3.12, CPython ran every comprehension as a hidden function call. PEP 709 inlined list, dict and set comprehensions into the code around them, which made them faster (up to twice as fast on the PEP’s own microbenchmark) without changing what they can see: the variables stay as isolated as before. Two details did change: a traceback no longer shows a separate <listcomp> line for an error inside a comprehension, and locals() called inside one now includes the variables of the function around it.

Checkpoint

Check: A comprehension's own scope

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    x = 10
    ys = [x for x in range(3)]
    print(x, ys)
    Show the answer: Question 1 of 2

    Answer: 10 [0, 1, 2]

    A comprehension runs in a scope of its own, so its x takes 0, 1 and 2 without touching the x of the module, which still holds 10.

  2. Question 2 of 2 What does this print?

    Read the code, then choose one answer.

    total = 0
    sums = [total := total + n for n in [1, 2, 3]]
    print(sums, total)
    Show the answer: Question 2 of 2

    Answer: [1, 3, 6] 6

    PEP 572 makes := bind in the enclosing scope, so total is 1, then 3, then 6, and the comprehension records each value.

Round brackets make a generator expression, which builds no list

Round brackets do not make a “tuple comprehension”. They make a generator expression: A comprehension in round brackets, such as (x * x for x in xs). It builds no list: it makes each item only when asked for the next one, and it can be read through only once., which produces its items one at a time, only when something asks for the next one, and never builds a list at all. When it is the only argument of a call, its own brackets can be dropped: sum(x * x for x in xs). Step through a small one as it is asked for its items:

Interactive · line by line A generator gives each item once Step through it and watch what each line prints. The generator squares makes its values only when something asks for the next one.

Program · gen_once.py

squares = (n * n for n in range(4))
print(next(squares))
print(next(squares))
rest = sum(squares)
print(rest)
print(list(squares))

Variables

After line 6: squares is <generator object>; rest is 13.squares<generator object>rest13
  • changed in this step

Printed so far

0
1
13
[]

Every step was recorded from a run with Python 3.14.8 on macOS 26 arm64.

The drawing shows the variables when the program ends, and the program's 7 steps are listed below.

Line run 6
Next line end
Every step as text
  1. Start. No line has run yet, so there are no variables. Python starts with line 1.
  2. Line 1. squares = (n * n for n in range(4)) sets squares to <generator object>. Nothing is computed yet. The round brackets make a generator object, which will work out n * n for each n when asked.
  3. Line 2. print(next(squares)) prints 0. next() asks for one item. The generator runs just far enough to make 0 * 0 and hands it over.
  4. Line 3. print(next(squares)) prints 1.
  5. Line 4. rest = sum(squares) sets rest to 13. sum() asks for every item that is left, 2 * 2 and 3 * 3, so rest is 4 + 9 = 13.
  6. Line 5. print(rest) prints 13.
  7. Line 6. print(list(squares)) prints []. That was the last line, so the program ends. Key point The generator has nothing left to give, so list() gets no items. A generator can be read through only once.

The generator handed over 0 and then 1, one call of next() each; sum() took the two squares that were left, 4 + 9 = 13; and list() found nothing at all. What a generator saves is memory, which this program measures for 1,000 and for 1,000,000 squares:

A list of squares against a generator of squares Python · generator_memory.py
import sys

# getsizeof counts the container itself: for a list, its array of references
# (8 bytes each on a 64-bit build), not the int objects they point to.
for n in (1_000, 1_000_000):
    squares_list = [i * i for i in range(n)]  # every square, built now
    squares_gen = (i * i for i in range(n))  # a recipe: squares made one at a time
    print(f"{n:>9,} squares  list: {sys.getsizeof(squares_list):>9,} bytes  generator: {sys.getsizeof(squares_gen)} bytes")

# sum() takes the generator expression directly, without extra brackets.
print(sum(i * i for i in range(n)) == sum(squares_list))

# A generator runs once: a second pass finds it empty.
print(sum(squares_gen) > 0, sum(squares_gen))

Output

    1,000 squares  list:     8,856 bytes  generator: 208 bytes
1,000,000 squares  list: 8,448,728 bytes  generator: 208 bytes
True
True 0

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 generator_memory.py

The list grew from 8,856 bytes to 8,448,728 bytes, about a thousand times more for a thousand times more squares, while the generator stayed at 208 bytes. The list object alone holds a million references, about 8 MB on a 64-bit build, before counting the integers they point to; sys.getsizeof() measures only the container. The generator is a couple of hundred bytes, whatever its length, because it keeps only its position. This example shows its output from a recorded run rather than a Run button: in the browser, Python is a 32-bit build, so every size would be different.

Two rules follow. When the values are used once, by sum(), max(), any(), all() or a for loop, a generator expression saves the memory of the list. When you need the values twice, their number or an item by position, build the list: the last line of the program shows that a generator, once used, is empty.

Checkpoint

Check: Generator expressions

  1. Question 1 of 2 What does this print?

    Read the code, then choose one answer.

    g = (n for n in range(3))
    print(sum(g), sum(g))
    Show the answer: Question 1 of 2

    Answer: 3 0

    A generator expression makes its items once, on demand. After the first sum() it is exhausted, so the second one adds nothing and returns 0.

  2. Question 2 of 2 In which case should you build a list rather than a generator expression?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: You need the number of values and then their average

    Build the list when you need the values more than once, their count or an item by position; use a generator expression when they are consumed once.

A loop reads better once a comprehension needs statements

A comprehension is at its best when it fits on one line and does one thing: transform, filter, or both. Beyond that it hides what a loop would show, and a loop can hold statements that a comprehension cannot, such as try and except:

An over-clever comprehension and the loop that replaces it Python · over_clever.py
lines = ["asha: 91", "", "# absent today", "ravi: 67", "meera: 104", "john: n/a"]

# One expression, three clauses and a nested conditional: it works,
# but every reader has to take it apart first, and "john: n/a" would
# crash it if the isdigit() test were forgotten.
clever = {
    name.strip().title(): min(int(score), 100) if score.strip().isdigit() else None
    for line in lines
    if line and not line.startswith("#")
    for name, score in [line.split(":")]
}
print(clever)

# The same rules as a loop: one step per line, room for a comment
# and for the error handling that a comprehension cannot hold.
plain = {}
for line in lines:
    if not line or line.startswith("#"):
        continue  # blank lines and comments carry no marks
    name, score = line.split(":")
    try:
        plain[name.strip().title()] = min(int(score), 100)  # 100 is the maximum
    except ValueError:
        plain[name.strip().title()] = None  # not a number
print(plain == clever)

# A comprehension used only for its side effect builds a useless list.
shown = [print(name) for name in plain]
print(shown)

Output

{'Asha': 91, 'Ravi': 67, 'Meera': 100, 'John': None}
True
Asha
Ravi
Meera
John
[None, None, None, None]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 over_clever.py

The comprehension at the top is correct, and it is still a poor choice. It needs three clauses, a nested conditional and the trick of looping over a one-item list, for name, score in [line.split(":")], just to give a value a name. The loop gives every rule its own line, and it handles a score such as "n/a" with try and except, which no comprehension can contain. The last lines show the other misuse: a comprehension written only for its A change a piece of code makes besides the value it returns, such as printing, writing a file or appending to another list., here printing, builds a list of None values that nobody needs. Write a for loop when nothing should be returned.

A few habits keep comprehensions readable:

  • At most two clauses, say one for and one if, or two for clauses. More than that reads better as a loop.
  • No side effects: no printing, no appending to another list, no changes to the items.
  • A helper function for any logic that needs more than one condition, like grade() above.
  • Speed is rarely the reason to choose. Comprehensions avoid looking up and calling append for every item, but the work done per item usually costs more than the loop around it, so choose the form that reads best.

Checkpoint

Check: When a loop is clearer

  1. One question Which job needs a for loop rather than a comprehension?

    Choose one answer.

    Show the answer: One question

    Answer: Turning texts into numbers, with None for any text that raises ValueError

    A comprehension holds expressions: a transformation, a filter, both. Anything that needs statements, such as try and except, belongs in a loop.

Practise: word lengths and a flat list

The exercise asks for one dict comprehension and one list comprehension: a map from every cleaned word of a text to its length, and the items of a table flattened row by row. One of its sample tests reads your code to make sure the comprehensions are there, and the first hint suggests cleaning the words before deciding which to keep:

Exercise · Easy · Python

Word lengths and a flat list, with comprehensions

Write two functions in words.py, each with a comprehension:

  • word_lengths(text) returns a dictionary that maps every word of text to its length. Split the text on whitespace, remove punctuation from both ends of each word (string.punctuation lists it), and fold its case with casefold(), so "The", "the," and "THE!" are all the word "the". A word that is empty after its punctuation is removed, such as "--", is left out. Punctuation inside a word stays: "don't" has length 5.
  • flatten_matrix(matrix) takes a list of lists and returns one flat list of their items, row by row: [[1, 2], [3]] gives [1, 2, 3]. Rows may be empty, and so may the matrix.

word_lengths must build its result with a dictionary comprehension and flatten_matrix with a list comprehension; one of the sample tests reads words.py to check that they do.

Starter code · words.py

import string


def word_lengths(text):
    """Map each word of text, case-folded and without punctuation at its ends, to its length."""
    # Replace this line with a dictionary comprehension.
    return {}


def flatten_matrix(matrix):
    """Return the items of a list of lists as one flat list, row by row."""
    # Replace this line with a list comprehension.
    return []
The sample tests · test_words.py
import ast

import words
from words import flatten_matrix, word_lengths


def test_simple_sentence():
    """maps each word to its length"""
    assert word_lengths("sun rises early") == {"sun": 3, "rises": 5, "early": 5}


def test_case_and_punctuation():
    """folds case and removes punctuation at both ends of a word"""
    assert word_lengths("The cat, the HAT! (the end)") == {"the": 3, "cat": 3, "hat": 3, "end": 3}
    assert word_lengths("Straße STRASSE") == {"strasse": 7}


def test_inner_punctuation_and_empty_words():
    """keeps punctuation inside a word and leaves out words that are only punctuation"""
    assert word_lengths("don't -- stop... e-mail") == {"don't": 5, "stop": 4, "e-mail": 6}
    assert word_lengths("") == {}
    assert word_lengths("  ... !!! ") == {}


def test_flatten():
    """flattens rows in order, including empty rows"""
    assert flatten_matrix([[1, 2], [3], [], [4, 5, 6]]) == [1, 2, 3, 4, 5, 6]
    assert flatten_matrix([]) == []
    assert flatten_matrix([[], []]) == []
    grid = [["a"], ["b", "c"]]
    assert flatten_matrix(grid) == ["a", "b", "c"]
    assert grid == [["a"], ["b", "c"]], "flatten_matrix must not change its argument"


def _function(tree, name):
    for node in tree.body:
        if isinstance(node, ast.FunctionDef) and node.name == name:
            return node
    raise AssertionError(f"words.py has no function {name}()")


def test_uses_comprehensions():
    """builds the results with a dict comprehension and a list comprehension"""
    with open(words.__file__, encoding="utf-8") as source:
        tree = ast.parse(source.read())
    lengths = _function(tree, "word_lengths")
    flatten = _function(tree, "flatten_matrix")
    assert any(isinstance(n, ast.DictComp) for n in ast.walk(lengths)), "word_lengths() needs a dict comprehension"
    assert any(isinstance(n, ast.ListComp) for n in ast.walk(flatten)), "flatten_matrix() needs a list comprehension"
A hint

Clean each word first, then decide whether to keep it: raw.strip(string.punctuation).casefold() gives the cleaned word, and an if word clause leaves out the empty ones. A list comprehension of cleaned words, followed by a dict comprehension over that list, keeps each step readable. For the matrix, two for clauses in one comprehension read like two nested loops: first the rows, then the items of each row.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Python Online Compiler Rewrite one of your own loops as a comprehension and check that both print the same result.

Seven ideas, from the parts of a comprehension to the point where a loop takes over, are in the self-check below. It starts with the one your answers mark as the weakest, and every choice carries its own reason:

Practice that adapts to you

Practise this lesson

19 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 19 What does [m for m in marks if m >= 40] do to the list marks?

    Choose one answer.

    Show the answer: Question 1 of 19

    Answer: Nothing; it builds and returns a new list

    A comprehension always builds a new list from the items it reads, and the list it reads stays exactly as it was.

  2. Question 2 of 19 What does this print?

    Read the code, then choose one answer.

    marks = [38, 52, 39]
    graced = [m + 2 for m in marks]
    print(graced, [m for m in graced if m >= 40])
    Show the answer: Question 2 of 19

    Answer: [40, 54, 41] [40, 54, 41]

    The first comprehension has no if clause, so every item is transformed. The second reads its result and filters it; since 40, 54 and 41 all pass, nothing is left out.

  3. Question 3 of 19 In [r for r in readings if r != -999], what does the if clause do?

    Choose one answer.

    Show the answer: Question 3 of 19

    Answer: It decides whether each reading is included in the new list

    After the for clause, if is a filter: each reading is tested and kept only when the test is true, so the new list can be shorter than readings.

  4. Question 4 of 19 Which of the lines that build a, b and c gives [4, 0, 7, 0]?

    Read the code, then choose one answer.

    xs = [4, -2, 7, -5]
    a = [x for x in xs if x > 0]
    b = [x if x > 0 else 0 for x in xs]
    c = [0 for x in xs if x <= 0]
    Show the answer: Question 4 of 19

    Answer: Line b

    An if ... else before for is a conditional expression: it runs for every item and chooses the value, so the result has four items. An if after for filters: line a keeps only [4, 7], and line c keeps the two negative numbers and writes 0 for each, giving [0, 0].

  5. Question 5 of 19 What does this print?

    Read the code, then choose one answer.

    xs = [120, -5, 50]
    print([x if x < 100 else 100 for x in xs if x > 0])
    Show the answer: Question 5 of 19

    Answer: [100, 50]

    Both forms can appear together: the filter after the for clause decides which items are used, and the conditional expression before it decides what each kept item becomes.

  6. Question 6 of 19 What does {x for x in []} give?

    Choose one answer.

    Show the answer: Question 6 of 19

    Answer: set(), an empty set

    {x for x in []} is a set comprehension with no items, so its value is set(), which is how Python prints an empty set.

  7. Question 7 of 19 What is {len(w): w for w in ["ox", "cat", "be", "dog"]}?

    Choose one answer.

    Show the answer: Question 7 of 19

    Answer: {2: 'be', 3: 'dog'}

    A dictionary holds each key once. The comprehension sets the value for key 2 twice and for key 3 twice, and the later assignment replaces the earlier one, so the last word of each length wins. To keep every word, build a list per key.

  8. Question 8 of 19 grades maps names to grades, and two students share an "A". Which comprehension maps each grade to every student who has it?

    Choose one answer.

    Show the answer: Question 8 of 19

    Answer: {g: [n for n, x in grades.items() if x == g] for g in "ABCD"}

    A value that must hold several names has to be a list. The program's last comprehension builds one list per grade, which keeps both students with an "A".

  9. Question 9 of 19 What does this print?

    Read the code, then choose one answer.

    pairs = [(r, c) for r in "ab" for c in "12"]
    print(pairs)
    Show the answer: Question 9 of 19

    Answer: [('a', '1'), ('a', '2'), ('b', '1'), ('b', '2')]

    Several for clauses nest from left to right, like loops written one inside the other: the first clause is the outer loop, so c runs through "12" for each r. The result is one flat list; a list of lists would need a comprehension inside the expression.

  10. Question 10 of 19 In [m for row in marks for m in row], which for clause is the outer loop?

    Choose one answer.

    Show the answer: Question 10 of 19

    Answer: The first one, for row in marks

    Write the loops out one inside the other, in the order of the clauses, and you get the same items in the same order: rows first, then the marks of each row.

  11. Question 11 of 19 What does this print?

    Read the code, then choose one answer.

    rows = [[1, 2], [3, 4], [5, 6]]
    print(list(zip(*rows)))
    Show the answer: Question 11 of 19

    Answer: [(1, 3, 5), (2, 4, 6)]

    zip(*rows) is zip([1, 2], [3, 4], [5, 6]), which gives the columns as tuples: (1, 3, 5) and (2, 4, 6). It is the built-in way to transpose a table.

  12. Question 12 of 19 A program that has not used the name x runs squares = [x * x for x in range(4)] and then print(x). What happens?

    Choose one answer.

    Show the answer: Question 12 of 19

    Answer: NameError, because the comprehension's x never leaves it

    This is the difference from a for loop, whose variable stays behind. Only names bound with := inside a comprehension reach the scope around it.

  13. Question 13 of 19 What does scope.py print?

    What does this program print? Choose one answer.

    x = "kept"
    squares = [x * x for x in range(4)]
    print(squares, x)  # the comprehension's x did not replace this one
    
    # An assignment expression (:=) is the exception: it binds in the
    # enclosing scope, so total is still there after the comprehension.
    total = 0
    running = [total := total + n for n in [5, 3, 8]]
    print(running, total)
    
    
    class Exam:
        max_marks = 50
        raw = [20, 35, 50]
        # The first iterable is evaluated in the class body, so raw is found.
        copied = [m for m in raw]
        # Everything else runs in the comprehension's own scope, which cannot
        # see the names of a class body.
        try:
            percent = [m * 100 // max_marks for m in raw]
        except NameError as err:
            print("NameError:", err)
    
    
    print(Exam.copied)
    Show the answer: Question 13 of 19

    Answer: it prints

    [0, 1, 4, 9] kept
    [5, 8, 16] 16
    NameError: name 'max_marks' is not defined
    [20, 35, 50]

    The comprehension's x lives in its own scope, so the outer x keeps the text "kept". An assignment expression binds in the enclosing scope, which is why total ends at 16. In a class body, only the first iterable (raw) is evaluated in the class's own scope; max_marks inside the expression cannot be seen, so the comprehension raises NameError.

  14. Question 14 of 19 What does this print?

    Read the code, then choose one answer.

    def initials():
        x = "outer"
        ys = [x for x in "ab"]
        return x, ys
    
    print(initials())
    Show the answer: Question 14 of 19

    Answer: ('outer', ['a', 'b'])

    A comprehension's scope sits inside the function's scope, so it can read the function's names, but its own loop variable shadows them only inside the brackets.

  15. Question 15 of 19 What does (x * x for x in xs) make?

    Choose one answer.

    Show the answer: Question 15 of 19

    Answer: A generator that works out each square when it is asked for the next one

    A generator expression keeps only its position and makes the next item on demand, which is why it takes the same small amount of memory whatever its length.

  16. Question 16 of 19 What does this print?

    Read the code, then choose one answer.

    print(sum(x * x for x in range(4)), max(len(w) for w in ["ox", "bee"]))
    Show the answer: Question 16 of 19

    Answer: 14 3

    sum(x * x for x in range(4)) and max(len(w) for w in ...) pass generator expressions straight to the call, which reads each value once and builds no list.

  17. Question 17 of 19 A function receives (line.strip() for line in lines) and calls len() on it. What happens?

    Choose one answer.

    Show the answer: Question 17 of 19

    Answer: TypeError, because a generator does not know how many items it will make

    Pass a list when the receiver needs the count, an item by position, or a second pass; pass a generator expression when it reads the values once.

  18. Question 18 of 19 What does shown = [print(n) for n in ["a", "b"]] leave in shown?

    Choose one answer.

    Show the answer: Question 18 of 19

    Answer: [None, None]

    A comprehension written only for its side effect builds a list nobody needs. A for loop is the right tool when nothing should be returned.

  19. Question 19 of 19 In which of these situations is a plain for loop the better choice than a comprehension?

    Choose every answer that is right.

    Show the answer: Question 19 of 19

    Answer:

    • The work for each item needs a try and except
    • The only purpose is to print each item

    A comprehension holds expressions, not statements, so try and except cannot go inside it, and one built only for a side effect such as print() creates a list of None values that nobody uses. Transforming every item and collecting distinct values are exactly what list and set comprehensions are for.

Interview questions

Fresher · What is the difference between [x for x in xs if x > 0] and [x if x > 0 else 0 for x in xs]?

The first one filters: the if after the for clause decides whether each item is included, so negative numbers are left out and the result can be shorter than xs. The second one has no filter. Its x if x > 0 else 0 is a conditional expression, the part that computes each item, so every item of xs produces one item of the result and the length stays the same: negative numbers become 0. A good answer adds that the two can be combined, as in [x if x < 100 else 100 for x in xs if x > 0], and that writing else after the for clause is a syntax error, because only a filter can go there.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A report computes total = sum([len(line) for line in log_file]) over a log of 50 million lines. On a small server it runs out of memory before printing anything. Explain why, fix it, and say when you would still build the list.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    This works at module level but raises NameError in a class body: class Grid: size = 8; cells = [(r, c) for r in range(size) for c in range(size)]. Which size fails, and why? Give two fixes, and say whether a generator expression or a lambda in the class body would behave differently.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • [expression for item in iterable if condition] builds a new list, {k: v for ...} a dictionary and {expression for ...} a set, and the iterable they read is never changed.
  • An if after for filters items out, while a if condition else b before for chooses a value for every item and keeps the length.
  • A dict comprehension keeps one value per key, so a key that comes again replaces the earlier value.
  • Several for clauses nest from left to right and give one flat list, while a comprehension inside the expression gives a list of lists.
  • A comprehension’s loop variables stay inside it, names bound with := are the exception, and in a class body only the first iterable can see the class’s names.
  • Round brackets make a generator expression, which produces items on demand in a few hundred bytes and can be read only once, and a for loop is the better tool once the work needs statements or side effects.

Terms in this lesson

comprehension
An expression in square brackets or braces that builds a new list, dict or set from the items of an iterable, such as [m for m in marks if m >= 40]. It runs a loop of its own and never changes what it reads.
generator expression
A comprehension in round brackets, such as (x * x for x in xs). It builds no list: it makes each item only when asked for the next one, and it can be read through only once.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.