Engineering Notes
Home/Languages/Python/Comprehensions
Languages · Python

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

💻 Languages● Beginner⏱ 6 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 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

python
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 loops

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

2Set and Dict Comprehensions

python
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

python
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 argument

A 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)

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

python
[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

python
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

python
# 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):

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

A list comprehension compiles to its own hidden function, called immediately with the source iterable as its argument
A list comprehension compiles to its own hidden function, called immediately with the source iterable as its argument

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

python
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

  1. Overusing nested comprehensions until they're unreadable — if you need more than one for or more than one if, strongly consider a regular loop instead (section 6).
  2. Confusing the filtering if with the ternary if/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.
  3. Building a full list comprehension just to sum/any/all/max it once — use a generator expression instead (sum(x for x in xs), not sum([x for x in xs])) to avoid the wasted intermediate list.
  4. 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

python
[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

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

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

Follow-ups interviewers ask
  • 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)).

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.

Follow-ups interviewers ask
  • 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.