← thecodex.expert · The Codex Family of Knowledge
🐍 Python Course · Stage 1 · Lesson 8 of 89 · Comprehensions
Course Overview →

🟩 Start with the Beginner tab. Read it fully before moving on.

← match / case Iterators & Iterables →
Python

Comprehensions in Python

List, dict, set, and generator comprehensions — Python's concise syntax for building collections from iterables in a single readable expression.

Python 3.13 docs.python.org Last verified:
Canonical Definition

A comprehension is a compact syntax for constructing a list, set, dict, or generator from one or more iterables, optionally filtering elements and transforming each one within a single expression, as defined in the Python Language Reference under displays for lists, sets, and dictionaries.

🟩 Beginner
🟩 Beginner

Python's most elegant shorthand

What you'll learn: How to build lists, dicts, and sets from existing sequences in one readable line instead of a for loop with append(). Once you know this, you'll use it in almost every Python file you write.

How to read this tab: For each comprehension example, first write the equivalent for loop in your head (or on paper), then look at the comprehension. The pattern is always: [expression for item in iterable]. Read left to right: "give me [expression], for each [item] in [iterable]."

⏱ 35 min 📄 2 sections 🔶 Prerequisite: Control Flow & Loops
The idea

A comprehension lets you build a new list (or dict, or set) from an existing sequence in one line, instead of writing a for loop with .append() on every iteration. It reads almost like English: "give me x squared, for each x in this range, where x is even."

⭐

The pattern to memorise: [expression for item in iterable if condition]. The if condition is optional. Read it right-to-left first: "for each item in iterable, where condition is true, give me expression." This is the single most-used Python construct after basic assignments and function calls.

List comprehensions

The list comprehension is the most common form. The syntax is [expression for item in iterable], with an optional if condition to filter. According to the Python Tutorial, a list comprehension consists of brackets containing an expression followed by a for clause, then zero or more for or if clauses.

Pythonlist_comprehensions.py
# The loop version
squares = []
for x in range(10):
    squares.append(x * x)

# The comprehension version — same result, one line
squares = [x * x for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

# With a filter — only even numbers
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

# Transform and filter together
names = ["priya", "arjun", "rohit", "kavya"]
capitalised = [name.title() for name in names if len(name) > 4]
# ['Priya', 'Arjun', 'Rohit', 'Kavya']

# Apply a function to each element
words = ["  hello  ", "  world  "]
cleaned = [w.strip() for w in words]
# ['hello', 'world']
Read it right to left, then left

To understand [x * x for x in range(10)], first read the for part (for each x in range 10), then the expression at the front (compute x times x). The result of the front expression is what gets collected into the new list.

💡

Same pattern, different brackets. {key: value for item in iterable} builds a dict. {expression for item in iterable} builds a set. The only difference from a list comprehension is the curly braces and the key: value syntax for dicts. Note: empty {} is a dict, not a set — use set() for an empty set.

Dict and set comprehensions

The same pattern works for dictionaries and sets — just change the brackets. Curly braces with a key: value expression build a dict; curly braces with a single expression build a set.

Pythondict_set_comprehensions.py
# Dict comprehension — {key: value for ...}
names = ["Priya", "Arjun", "Rohit"]
name_lengths = {name: len(name) for name in names}
# {'Priya': 5, 'Arjun': 5, 'Rohit': 5}

# Build a lookup table
squares = {n: n * n for n in range(1, 6)}
# {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

# Invert a dictionary (swap keys and values)
original = {"a": 1, "b": 2, "c": 3}
inverted = {value: key for key, value in original.items()}
# {1: 'a', 2: 'b', 3: 'c'}

# Set comprehension — {expression for ...}, no key:value
numbers = [1, 2, 2, 3, 3, 3, 4]
unique_squares = {n * n for n in numbers}
# {16, 1, 4, 9}  — duplicates removed automatically

✅ Beginner tab complete — check your understanding

  • I can write a list comprehension that transforms every item in a list
  • I can write a list comprehension with an if condition to filter items
  • I can write a dict comprehension to build a key-value mapping from a list
  • I can write a set comprehension to get unique values

Continue to Functions & Scope →

🔵 Intermediate
🔵 Intermediate

Generator expressions and nested comprehensions

What you'll learn: Generator expressions — the lazy, memory-efficient alternative to list comprehensions that produces values one at a time. Nested comprehensions for working with 2D data. When to use which form.

How to read this tab: The key question for every comprehension you write: do I need the whole list at once, or am I just iterating once? If the latter, use a generator expression.

⏱ 35 min 📄 2 sections 🔶 Prerequisite: Beginner tab
⭐

The memory-saving version. Change square brackets to parentheses and you get a generator — values produced one at a time, not all at once. Rule of thumb: if you're passing the result directly to sum(), max(), min(), or any(), use a generator expression. It uses no more memory than a single item.

Generator expressions

A generator expression looks exactly like a list comprehension but uses parentheses instead of square brackets. The crucial difference: it does not build the whole collection in memory. Instead it produces items one at a time, on demand. The Python Tutorial notes that generator expressions are more memory-efficient than equivalent list comprehensions when you only need to iterate once.

Pythongenerator_expressions.py
# List comprehension — builds the entire list in memory
sum_squares = sum([x * x for x in range(1_000_000)])

# Generator expression — produces values one at a time, no list built
sum_squares = sum(x * x for x in range(1_000_000))
# When a generator expression is the sole argument to a function,
# the parentheses can be omitted.

# A generator object is lazy — nothing computed until iterated
gen = (x * x for x in range(5))
print(gen)            # <generator object ...>
print(next(gen))      # 0
print(next(gen))      # 1
print(list(gen))      # [4, 9, 16] — consumes the rest

# Generators are single-use — once exhausted, they yield nothing
gen = (x for x in range(3))
print(list(gen))      # [0, 1, 2]
print(list(gen))      # [] — already consumed
When to use which

Use a list comprehension when you need the actual list — to index into it, loop over it multiple times, or check its length. Use a generator expression when you only iterate once and the sequence is large (or infinite), to avoid holding everything in memory at once.

⚠

Readability warning. Nested comprehensions read left to right in the for clauses, which is the opposite of how nested loops look on the page. [n for row in matrix for n in row] is "for each row, for each n in that row" — the first for is the outer loop. If your comprehension has more than two for clauses, consider a regular loop instead.

Nested comprehensions and multiple clauses

A comprehension can contain multiple for clauses, which behave like nested loops. The leftmost for is the outermost loop. You can also nest a comprehension inside another comprehension's expression.

Pythonnested_comprehensions.py
# Multiple for clauses — flattening a 2D list
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [num for row in matrix for num in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Read left to right: for each row, for each num in row

# Equivalent nested loop (for comparison)
flat = []
for row in matrix:
    for num in row:
        flat.append(num)

# Nested comprehension — transpose a matrix
matrix = [[1, 2, 3], [4, 5, 6]]
transposed = [[row[i] for row in matrix] for i in range(3)]
# [[1, 4], [2, 5], [3, 6]]

# Cartesian product with a filter
pairs = [(x, y) for x in range(3) for y in range(3) if x != y]
# [(0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)]
Commonly confused
Brackets decide the type. [...] builds a list, {...} with key: value builds a dict, {...} with a single value builds a set, and (...) builds a generator — not a tuple. There is no "tuple comprehension"; (x for x in ...) is a generator expression. To build a tuple, wrap a generator: tuple(x for x in ...).
Multiple for clauses read left to right. In [n for row in matrix for num in row] the order matches nested loops top to bottom — the first for is the outer loop. This is the opposite of what some people expect.

✅ Intermediate tab complete — check your understanding

  • I know the difference between [x for x in ...] (list) and (x for x in ...) (generator)
  • I can use sum(x*x for x in range(n)) without building an intermediate list
  • I can write a nested comprehension to flatten a 2D list
  • I know that a generator is single-use — once exhausted, it yields nothing

Continue to Functions & Scope →

🔴 Expert
🔴 Expert

Scope rules, the walrus operator, and bytecode

What you'll learn: Why comprehension variables don't leak into the enclosing scope (unlike Python 2). How the walrus operator (:=, PEP 572) can bind names that DO escape. The LIST_APPEND bytecode optimisation that makes comprehensions faster than equivalent loops.

How to read this tab: Read after Stage 2. Useful when you're debugging unexpected NameErrors or optimising hot code paths.

⏱ 20 min 📄 2 sections 🔶 Prerequisite: After Stage 2
📎

Expert territory. This section explains a subtle Python 3 behaviour: comprehension variables are scoped to the comprehension, not the enclosing function. In Python 2, they leaked. This section also introduces the walrus operator (:=) which is the only way to intentionally bind a name in the enclosing scope from inside a comprehension.

Comprehension scope (PEP 572 and the comprehension namespace)

Since Python 3, comprehensions and generator expressions have their own enclosing scope. The iteration variable does not leak into the surrounding namespace — after [x for x in range(10)], the name x is not defined in the outer scope (in Python 2 it did leak; this was deliberately changed in Python 3). The Python Language Reference states that the comprehension is executed in a separate implicitly nested scope, which ensures that names assigned to in the target list do not "leak" into the enclosing scope.

Pythoncomprehension_scope.py
# The loop variable does not leak (Python 3)
result = [x * 2 for x in range(5)]
# print(x)  -> NameError: name 'x' is not defined

# The iterable expression of the LEFTMOST for is evaluated
# in the enclosing scope, the rest in the comprehension scope.
# This matters when the iterable raises:
data = [1, 2, 3]
squares = [y * y for y in data]   # 'data' resolved in enclosing scope

# Walrus operator (:=, PEP 572) CAN bind to the enclosing scope
# from inside a comprehension — useful for capturing a computed value
values = [10, 20, 30, 40]
filtered = [smoothed for v in values if (smoothed := v / 10) > 1]
# filtered == [2.0, 3.0, 4.0]; 'smoothed' leaks to enclosing scope
print(smoothed)   # 4.0 — walrus binding survives
📎

Why comprehensions are faster. CPython compiles list comprehensions to use a specialised LIST_APPEND opcode instead of looking up and calling the append method on each iteration. You can verify this with the dis module. In practice, comprehensions are 20-35% faster than equivalent for loops with .append() for the same reason.

Performance and the LIST_APPEND bytecode

A list comprehension is generally faster than an equivalent for loop with .append(), because CPython compiles it to use a specialised LIST_APPEND opcode rather than looking up and calling the append method on each iteration. The method-lookup overhead is eliminated. You can confirm this difference with the dis module, which shows the comprehension's bytecode is compiled into a nested code object.

Pythoncomprehension_bytecode.py
import dis

# A comprehension compiles to its own code object
def make_squares():
    return [x * x for x in range(10)]

dis.dis(make_squares)
# You will see LIST_APPEND in the nested code object,
# instead of LOAD_METHOD/CALL for .append()

# Generator expressions compile to a generator code object
# and use YIELD_VALUE — producing items lazily.
Readability is the real limit

The Python documentation and PEP 8 favour clarity. A comprehension that spans multiple for and if clauses can become harder to read than the explicit loop. If a comprehension needs more than two clauses or a complex expression, an ordinary loop is often the better choice — the goal is concise and readable, not merely short.

✅ Expert tab complete

  • I know that the loop variable in a comprehension does NOT leak into the enclosing scope in Python 3
  • I know that := (walrus operator) DOES bind to the enclosing scope from inside a comprehension
  • I understand why LIST_APPEND makes comprehensions faster than a loop with .append()

Continue to Functions & Scope →

Source confidence: High Last verified: Primary source: Python Language Reference §6.2.4

Sources

1
Python Language Reference §6.2.4 — Displays for lists, sets and dictionaries. docs.python.org/3/reference/expressions.html#displays-for-lists-sets-and-dictionaries.
2
Python Tutorial §5.1.3 — List Comprehensions. docs.python.org/3/tutorial/datastructures.html#list-comprehensions.
3
Python Tutorial §5.5 — Dictionaries; §6 — Generator expressions. docs.python.org/3/tutorial/.
4
PEP 572 — Assignment Expressions (the walrus operator). peps.python.org/pep-0572/.
5
Python Language Reference §4.2.3 — note on comprehension scope. docs.python.org/3/reference/executionmodel.html.