codejump

AcademyPython

List Comprehensions in Python

How a comprehension differs from a for-loop under the hood, when it trades readability for cleverness, and how the dict and set forms follow the same rule.

3 min readUpdated

3 exercises

Waiting at the end of this page.

What a comprehension actually is

A list comprehension is not syntax sugar layered on top of .append — it compiles to its own bytecode, with its own scope, and it builds the whole list in one pass:

python
squares = [n * n for n in range(10)]
python
squares = []
for n in range(10):
    squares.append(n * n)

Both produce the same list, but the comprehension is measurably faster: the loop body pays for a LOAD_METHOD and CALL_METHOD on every iteration to look up .append, where the comprehension has a single specialised opcode (LIST_APPEND) that the interpreter jumps to directly. On a hot path processing millions of items, that difference is not cosmetic.

The scope a comprehension gets

Since Python 3, a comprehension runs in its own implicit function scope. This is the part interviewers probe, because it changes what leaks:

python
n = 'outer'
squares = [n * n for n in range(3)]
print(n)  # 'outer' — the comprehension's n never touched the outer one

In Python 2, the loop variable leaked into the enclosing scope; in Python 3 it cannot, because the comprehension is its own frame. This is also why a comprehension can safely be used inside a class body without clashing with a class attribute of the same name — something a plain for loop at class scope cannot promise.

Filtering with the trailing if

python
evens = [n for n in range(20) if n % 2 == 0]

The if here filters — it decides whether n is emitted at all. That is a different job from the conditional expression form, which decides what gets emitted:

python
labels = ['even' if n % 2 == 0 else 'odd' for n in range(6)]

Mixing the two up is the most common comprehension bug: [x if cond for x in xs] is a syntax error, because a filtering if has no else and a conditional expression cannot be left without one.

Dict and set comprehensions follow the same rule

python
word_lengths = {word: len(word) for word in ['a', 'bb', 'ccc']}
unique_lengths = {len(word) for word in ['a', 'bb', 'ccc']}

Same scoping, same filtering if, same performance story — only the brackets and the key: value shape change. A dict comprehension silently drops duplicate keys, keeping the last value written, which matters when the source has repeats:

python
{n % 3: n for n in range(6)}
# {0: 3, 1: 4, 2: 5} — 0, 3 both map to key 0; 3 wins because it came later

Where nesting stops paying off

python
flat = [x for row in matrix for x in row]

The clauses read left to right, in the same order a nested for loop would use — the outer loop is first. That single line is a fair trade for the four-line loop it replaces. A comprehension that nests two for clauses plus a condition, or that nests one comprehension inside another, stops being more readable than the loop it replaced — at that point a generator expression with a name, or a plain loop, communicates the intent better than a one-liner that has to be read twice.

The comprehension you did not mean to write

python
result = [expensive_call(x) for x in huge_list]

If only the first three items of result are ever used, this line still ran expensive_call on all of them — a list comprehension is eager, not lazy. Wrapping the same expression in parentheses instead of brackets turns it into a generator expression, which computes one item at a time and is the right choice whenever the whole list is never needed at once.

Now practice it

2 challenges · 1 quiz

Reading this page is the cheap half. These are the exercises that make you use it.