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
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:
squares = [n * n for n in range(10)]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:
n = 'outer'
squares = [n * n for n in range(3)]
print(n) # 'outer' — the comprehension's n never touched the outer oneIn 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
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:
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
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:
{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 laterWhere nesting stops paying off
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
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 quizReading this page is the cheap half. These are the exercises that make you use it.
- List Comprehension — Squares FilterA filter-and-transform comprehension — the exact shape the trailing if is built for.ChallengeSolve · EasyChallengeSolve · Easy
- Dictionary Comprehension — Word FrequencySame rules, dict form — including what happens when two source keys collide.ChallengeSolve · EasyChallengeSolve · Easy
- Fundamentals · Core ConceptsCore syntax questions, including the comprehension scoping this page explains.QuizTake · EasyQuizTake · Easy
Read next
Generators and Iterators in Python
The iterator protocol every for-loop relies on, what yield actually suspends, and why a generator can process a file too large to fit in memory.
3 min
Slicing and Mutability in Python
How start:stop:step slicing works on any sequence, why a slice always copies while an index into a list of lists does not, and where is diverges from ==.
3 min