Comprehensions
A comprehension is a concise expression for building a new list from an iterable, optionally filtering (if) and optionally nesting multiple for clauses (equivalent to nested loops, outermost first).
🎯 What you'll learn
- List Comprehensions
- Set and Dict Comprehensions
- Generator Expressions — Lazy, Not Eager
- Conditional Expression Inside a Comprehension (Not a Filter)
- Nested Comprehensions
1List Comprehensions
squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
evens = [x for x in range(20) if x % 2 == 0]
# [0, 2, 4, ..., 18]
pairs = [(x, y) for x in range(3) for y in range(2)]
# [(0,0), (0,1), (1,0), (1,1), (2,0), (2,1)] — left-to-right nesting, same as nested for loopsA comprehension is a concise expression for building a new list from an iterable, optionally filtering (if) and optionally nesting multiple for clauses (equivalent to nested loops, outermost first).
2Set and Dict Comprehensions
unique_lengths = {len(word) for word in ["a", "bb", "cc", "ddd"]}
# {1, 2, 3} — duplicates collapse, like any set
word_lengths = {word: len(word) for word in ["a", "bb", "ccc"]}
# {"a": 1, "bb": 2, "ccc": 3}Same syntax pattern, different brackets/delimiter: {...} with : is a dict comprehension; {...} without : is a set comprehension; [...] is a list comprehension.
3Generator Expressions — Lazy, Not Eager
gen = (x ** 2 for x in range(1_000_000)) # parentheses, not brackets
print(type(gen)) # <class 'generator'>
total = sum(x ** 2 for x in range(1_000_000)) # parens can be dropped when it's the sole argumentA generator expression looks like a list comprehension but does not build the whole collection in memory. It produces values one at a time, on demand, as something iterates over it. For a one-time pass over large or unbounded data (summing, searching for the first match, feeding into another iterator), this is far more memory-efficient than building an intermediate list just to throw it away.
4Conditional Expression Inside a Comprehension (Not a Filter)
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
# ["even", "odd", "even", "odd", "even"]This is different from the filtering if in section 1 — a trailing if...else before the for is a ternary expression that transforms every element; a trailing if after the for (with no else) filters which elements are included at all.
[x if x % 2 == 0 else -1 for x in range(5)] # transform: [0, -1, 2, -1, 4] (len == 5)
[x for x in range(5) if x % 2 == 0] # filter: [0, 2, 4] (len == 3)5Nested Comprehensions
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [n for row in matrix for n in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
transposed = [[row[i] for row in matrix] for i in range(3)]
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]Read multi-for comprehensions left to right, exactly like the equivalent nested for loops — the leftmost for is the outermost loop.
6When Not to Use a Comprehension
# hard to read — nested logic, side effects, multiple conditions
result = [transform(x) for x in data if validate(x) if x.status == "active" for y in x.children if y.enabled]
# clearer as a regular loop
result = []
for x in data:
if not validate(x) or x.status != "active":
continue
for y in x.children:
if y.enabled:
result.append(transform(x))A comprehension should read like a sentence. Once it needs more than one if/for clause or the logic inside stops being a simple expression, a plain loop is more maintainable — "can I write it as a comprehension" and "should I" are different questions.
Under the HoodComprehensions Compile to Their Own Function▾
A comprehension gets its own code object and its own scope
Since Python 3, every list/set/dict comprehension and generator expression is compiled as an implicit nested function with its own code object — not inlined into the enclosing function's bytecode. This is exactly why the comprehension's loop variable doesn't leak into the enclosing scope (Variables and Scope topic, section 6):
def demo():
x = "outer"
result = [x for x in range(3)]
return x # still "outer" — the comprehension's x lived in its own frame
print(demo()) # "outer"At the bytecode level, dis.dis on a function containing a list comprehension shows a MAKE_FUNCTION for the comprehension's own code object, followed by a CALL that immediately invokes it with the outer iterable as its argument — the comprehension body runs as a genuine (if invisible) function call, complete with its own local variable slots.
List/set/dict comprehensions still build the whole thing eagerly
Despite having their own scope like a generator expression, [...], {...}, and {k: v} comprehensions still eagerly iterate the entire source and build the complete result object before returning it — the "own scope" optimization is unrelated to laziness. Only the parenthesized generator expression form is lazy.
Generator expressions compile to an actual generator function
gen = (x * 2 for x in range(3))
print(type(gen)) # generator
print(gen.__class__.__name__) # 'generator'A generator expression's hidden function body doesn't return a completed collection — it contains yield, making the hidden function a generator function. Calling it doesn't run any of the loop body immediately; it returns a suspended generator object (see the Generators and Iterators topic for exactly how frame suspension works), and each element is only computed when something calls next() on it. This is the real mechanical reason sum(x**2 for x in huge_range) never materializes a million-element list: nothing eagerly builds anything, the generator produces exactly one value per next() call, consumed and discarded by sum as it goes.
⚠️ Common Pitfalls
- Overusing nested comprehensions until they're unreadable — if you need more than one
foror more than oneif, strongly consider a regular loop instead (section 6). - Confusing the filtering
ifwith the ternaryif/else—[x if cond else y for x in xs]keeps every element (transformed);[x for x in xs if cond]drops elements. Mixing them up silently changes the result length. - Building a full list comprehension just to sum/any/all/max it once — use a generator expression instead (
sum(x for x in xs), notsum([x for x in xs])) to avoid the wasted intermediate list. - Re-using a generator expression — once exhausted, a generator cannot be restarted;
gen = (x for x in range(3)); list(gen); list(gen)gives[0,1,2]then[]. A list comprehension doesn't have this problem since it's a real, re-readable list.
📋 Quick Reference
[expr for x in iterable] # list comprehension — eager
{expr for x in iterable} # set comprehension — eager, dedups
{k: v for x in iterable} # dict comprehension — eager
(expr for x in iterable) # generator expression — LAZY
[expr for x in iterable if cond] # filter
[a if cond else b for x in iterable] # transform (ternary), no filtering
[y for row in matrix for y in row] # flatten nested — left-to-right = outer-to-inner🎤 Interview Q&A
Q1What are comprehensions?core▾
Concise expressions building collections: list [f(x) for x in xs if cond], dict {k: v for k, v in pairs}, set {x for x in xs}, and generator expressions (f(x) for x in xs).
- Why prefer them over map/filter? More readable for most Python developers.
Q2List comprehension vs generator expression?core▾
List comprehensions build the whole list in memory; generator expressions produce items lazily, using constant memory, ideal for large data or when consumed once (sum(x*x for x in data)).
- Can you iterate a generator twice? No; it's exhausted.
Q3When do comprehensions hurt readability?core▾
Multiple nested loops with conditions or side effects. Use regular loops or helper functions when it doesn't fit comfortably on a line or two.
- Side effects in comprehensions? Avoid; use a for loop.
Q4How do nested comprehensions work?senior▾
Loops read left to right like nested for statements: [cell for row in grid for cell in row] flattens a 2D list.
- Transpose a matrix?
[list(col) for col in zip(*matrix)].
Q5Do comprehensions have their own scope?core▾
Yes, in Python 3 loop variables don't leak; the walrus operator (:=) can bind names in the enclosing scope.
- Walrus example?
[y for x in data if (y := f(x)) > 0].
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.