Python List Comprehensions: If Else, Nested and Dict Forms
A list comprehension builds a list from a loop, a filter and an expression in one line. Filters vs if-else, nested loops, and the set, dict and generator forms.
- Course: Python study plan
- Module: Lists, tuples and sequences
- Kind: Lesson
- Reading time: 13 min
- Runtime: CPython 3.11
What is a list comprehension in Python?
A list comprehension is an expression that builds a new list from an iterable in one line: [f(x) for x in xs if p(x)]. The expression comes first, the for clause supplies the elements and the optional if filters them. It replaces the create, loop, test and append pattern, has its own scope so the loop variable does not leak, and has set, dict and generator forms.
Lesson
A list comprehension is a loop, a filter and a transform folded into one expression that builds a list: [f(x) for x in xs if p(x)]. It replaces the four-line pattern of creating an empty list, looping, testing and appending, and it says what the result is rather than how it is assembled. Python has the same shape for sets and dictionaries and a lazy version for generators. This lesson teaches the syntax, the nested forms, the readability limit past which a loop is better, the scoping rule, and where a comprehension is the wrong tool — when the loop body has side effects, or when the result is thrown away.
The basic form
squares = [] # the loop
for x in range(6):
if x % 2 == 0:
squares.append(x * x)
squares = [x * x for x in range(6) if x % 2 == 0] # the comprehension: [0, 4, 16]
Read it as: the list of x * x for each x in range(6) such that x is even. The expression comes first, the for supplies the elements, the optional if filters. The expression can be anything — a call, a tuple, a conditional expression:
labels = ["even" if x % 2 == 0 else "odd" for x in xs] # a conditional expression: always produces
pairs = [(x, x * x) for x in xs]
lengths = [len(w) for w in words]
cleaned = [w.strip().lower() for w in words if w.strip()]
Note the two positions of if: if after the for filters (produces fewer elements); a conditional expression before the for chooses a value (produces one per element).
Nested loops
Several for clauses nest in reading order, left to right — the first for is the outer loop:
pairs = [(a, b) for a in range(3) for b in range(2)]
# [(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
flat = [x for row in grid for x in row] # flatten one level: outer loop over rows, inner over elements
triangles = [(a, b) for a in range(1, 5) for b in range(a, 5)] # the inner range may use the outer variable
A comprehension inside a comprehension builds nested output:
grid = [[0] * cols for _ in range(rows)] # a fresh row per iteration (Module 6 lesson 1)
table = [[r * c for c in range(1, 4)] for r in range(1, 4)]
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
Two nested levels is the readable limit. Three for clauses, or a comprehension inside a comprehension inside a comprehension, is a loop written as a puzzle — expand it.
Set, dict and generator forms
unique_lengths = {len(w) for w in words} # a set: braces, one expression
index = {w: i for i, w in enumerate(words)} # a dict: key: value
squares_by_x = {x: x * x for x in range(4)}
total = sum(x * x for x in xs) # a generator: no list is built
first_neg = next((x for x in xs if x < 0), None)
The dict comprehension is the idiom for inverting a mapping ({v: k for k, v in d.items()}) and for building lookups. The generator expression (Module 11) is the same syntax in parentheses and produces values lazily — the right form when the consumer (sum, max, any, "".join) only needs to see each value once, because no intermediate list is allocated. When a generator is the sole argument to a call, its parentheses double as the call's: sum(x for x in xs).
Comprehension versus map/filter versus loop
[int(t) for t in tokens] # comprehension
list(map(int, tokens)) # map — fine when the function already exists
[x for x in xs if x > 0] # comprehension
list(filter(lambda x: x > 0, xs)) # filter with a lambda — the comprehension is clearer
Reach for map when the function exists (int, str.strip, a def); reach for the comprehension whenever a lambda would be needed or a filter and a transform combine. Write a plain for loop when the body does something other than produce a value — printing, updating several variables, breaking early — and never write a comprehension whose result you discard just to run its side effects ([print(x) for x in xs] is a loop wearing a costume).
Scope
A comprehension has its own scope: the loop variable does not leak.
x = "outer"
squares = [x * x for x in range(3)]
print(x) # 'outer' — untouched (in Python 2 it would have been 2)
The comprehension can read enclosing names ([x * factor for x in xs]), and the walrus operator binds in the enclosing scope: [y for x in xs if (y := f(x)) > 0] leaves y defined afterwards — occasionally useful for "compute once, filter and keep".
Performance
A comprehension is somewhat faster than the equivalent append loop because the interpreter uses a specialised instruction for the append, and it allocates the list once when it can predict the size. That is a constant-factor gain, not an algorithmic one; a comprehension containing x in other_list is still quadratic. The generator form uses O(1) memory where the list form uses O(n) — the difference that matters at scale.
Pitfalls
- A comprehension for side effects (
[print(x) …]). - Three or more nested clauses; a conditional expression with an
iffilter and a nested comprehension on one line. - Putting the filter
ifin front of thefor(syntax error) or the conditional expression after it (also wrong). [[0] * n] * minstead of a comprehension for the rows.- A list comprehension where a generator would do —
sum([x for x in xs])builds a list for nothing. - Expecting the loop variable to be visible after the comprehension.
Key takeaways
[expr for x in iterable if cond]— expression, loop, filter; a conditional expression in theexprposition chooses a value per element.- Several
forclauses nest left to right; nested comprehensions build nested lists — a fresh inner list each time. {…}builds a set or a dict (k: v);(…)is a lazy generator, ideal as the argument tosum,max,any,join.- Prefer
mapwhen the function exists, the comprehension when a lambda would be needed, a loop when there are side effects or early exit. - Comprehensions have their own scope; keep them to two levels.
Common questions
How do I use if else in a list comprehension?
Put a conditional expression before the for: ["even" if x % 2 == 0 else "odd" for x in xs] produces one value per element. An if after the for is a filter that drops elements, and it cannot take an else.
How do nested list comprehensions work?
Several for clauses nest left to right, the first being the outer loop, so [x for row in grid for x in row] flattens a grid. A comprehension inside another, such as [[0] * cols for _ in range(rows)], builds nested lists. Past two levels, a plain loop reads better.
What is the difference between a list comprehension and a generator expression?
A list comprehension in square brackets builds the whole list in memory; a generator expression in parentheses produces values lazily, one at a time, in constant memory. Pass a generator to a consumer that reads each value once, as in sum(x * x for x in xs), max or any.
Is a list comprehension faster than a for loop in Python?
Somewhat. The interpreter uses a specialised instruction for the append and can allocate the list once, so a comprehension beats the equivalent append loop by a constant factor. The algorithm does not change: a comprehension that tests x in other_list is still quadratic.
How do I write a dictionary comprehension in Python?
Use braces with a key: value expression: {w: i for i, w in enumerate(words)} maps each word to its position, and {v: k for k, v in d.items()} inverts a mapping. Braces around a single expression, {len(w) for w in words}, build a set instead.
Exercises
Comprehension drills
From one line of integers produce four results, each with a single comprehension: the squares of the even values; a label per value (pos, neg or zero) using a conditional expression; the number of pairs (a, b) with a < b taken by index order from the list (a nested for with a filter, counted with len); and the sum of the absolute values as a generator expression inside sum.
Input: one line of integers. Output: squares: <values>, labels: <words>, pairs: <n>, abs-sum: <n> (an empty list prints squares: with nothing after it).
1 -2 3 0
prints
squares: 4 0
labels: pos neg pos zero
pairs: 3
abs-sum: 6Word index
Read one line of words. Print the distinct words in sorted order (a set comprehension over the lower-cased words, then sorted); a mapping from each distinct word to its length in first-seen order (a dict comprehension over dict.fromkeys(words), which keeps order); and the total number of characters (a generator expression in sum).
Input: one line of words. Output: unique: <sorted words>, lengths: <word=len pairs>, total: <n>.
Kiwi apple kiwi Fig
prints
unique: apple fig kiwi
lengths: kiwi=4 apple=5 fig=3
total: 16In this module: Lists, tuples and sequences
- Lists — the mutable sequence
- Comprehensions — building collections from expressions (this lesson)
- Sorting — sort, sorted, keys, stability and bisect
- Tuples, unpacking and named tuples
- Grids and nested lists
- The sequence tools — enumerate, zip, reversed, any, all, deque and the protocol
- Checkpoint — Lists, tuples and sequences
← Lists — the mutable sequence · Sorting — sort, sorted, keys, stability and bisect →