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.
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]."
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.
# 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']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.
# 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
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.
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.
# 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 consumedUse 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.
# 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)][...] 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 ...).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
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.
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.
# 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 survivesWhy 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.
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.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()