Topic 4.7
List, Dict and Set Comprehensions
In one line
A comprehension builds a new list, dictionary or set from another collection in one readable line: what to produce, where to take items from, and an optional filter. It replaces the common loop-and-append pattern, as long as it stays short enough to read at a glance.
Think of it like this
A teacher says "write down the square of each number on the board, but only the even ones". She does not describe a loop with an empty page and a pen; she states the result she wants in one sentence. A comprehension is that sentence in Python: [n * n for n in numbers if n % 2 == 0].
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Comprehension
- A short expression that builds a list, dictionary or set from another collection.
- Expression
- A piece of code that produces a value, such as
n * norlen(w). - Filter
- The optional
ifat the end of a comprehension, which keeps only some items. - Conditional expression
a if condition else b: givesawhen the condition is true, otherwiseb.- Nested comprehension
- A comprehension with more than one
for, or one inside another. - Flatten
- Turn a list of lists into one list with all the items.
- Scope
- The part of a program where a name can be used. A comprehension's loop variable lives only inside it.
- Generator expression
- A comprehension in round brackets that produces items one at a time instead of a full list.
- Side effect
- Anything a piece of code does besides giving a value, such as printing or changing a list.
Step by step
01From a loop to a comprehension
The four-line loop and the one-line comprehension below it do the same job. The comprehension puts the result first (n * n), then says where the items come from (for n in numbers).
numbers = [1, 2, 3, 4, 5, 6]
# the loop way
squares = []
for n in numbers:
squares.append(n * n)
# the comprehension way
squares = [n * n for n in numbers]02Filter with if
The if at the end decides which items get through. Only items where the condition is true reach the expression.
numbers = [1, 2, 3, 4, 5, 6]
evens = [n for n in numbers if n % 2 == 0]
big_squares = [n * n for n in numbers if n > 3]
print(evens) # [2, 4, 6]
print(big_squares) # [16, 25, 36]03if-else goes in front
When you want to change an item instead of dropping it, the choice goes in the expression part. Compare the two positions: if at the end filters, if ... else at the front chooses a value.
numbers = [1, 2, 3, 4]
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
print(labels) # ['odd', 'even', 'odd', 'even']04Dictionary and set comprehensions
Curly brackets with key: value build a dictionary. Curly brackets with a single expression build a set (duplicates vanish). Remember to print a set with sorted(...).
words = ["apple", "Banana", "cherry", "avocado"]
lengths = {w: len(w) for w in words}
initials = {w[0].lower() for w in words}
print(lengths)
print(sorted(initials)) # ['a', 'b', 'c']05Two for clauses
Multiple for clauses act like nested loops, outer first. This makes every pair, and also flattens a grid.
pairs = [(x, y) for x in [1, 2, 3] for y in "ab"]
matrix = [[1, 2, 3], [4, 5, 6]]
flat = [v for row in matrix for v in row]
print(len(pairs), flat) # 6 [1, 2, 3, 4, 5, 6]06The loop variable stays inside
The x inside the comprehension is a different x from the one outside. This protects your variables from being overwritten by accident. A normal for loop does not give this protection.
x = "outer"
squares = [x * x for x in range(3)]
print(x) # still outer07When not to use one
Three filters and two loops in one line is a puzzle, not code. If a teammate needs more than a few seconds to say what a comprehension produces, split it into a loop with clearly named steps. Clarity beats cleverness.
Try it yourself
- 1
Filter versus choose
In the first example, change
labelsso it keeps only the odd numbers (usingifat the end), then change it back to labelling all of them (usingif ... elseat the front). Say out loud what is different about the length of the result. - 2
Write one yourself
Make a list of the first 5 square numbers with
range. Then make a dictionary that maps each number from 1 to 5 to its cube. Check against[1, 4, 9, 16, 25]and{1: 1, 2: 8, 3: 27, 4: 64, 5: 125}.
Code & diagrams
Change the filter to n > 2 or the labels to your own words, and predict the output before running.
Expected output
squares: [1, 4, 9, 16, 25, 36]
evens: [2, 4, 6]
labels: ['odd', 'even', 'odd', 'even', 'odd', 'even']
upper: ['APPLE', 'BANANA', 'CHERRY', 'AVOCADO']
long words: ['Banana', 'cherry', 'avocado']
pairs: [(1, 'a'), (1, 'b'), (2, 'a'), (2, 'b'), (3, 'a'), (3, 'b')]
flat: [1, 2, 3, 4, 5, 6]
sum of squares: 91In the last lines, `[0] * 3` makes a list of three zeros, and the comprehension builds a fresh row each time. Topic 4.9 explains why that matters.
Expected output
lengths: {'apple': 5, 'Banana': 6, 'cherry': 6, 'avocado': 7}
initials: ['a', 'b', 'c']
cheap: {'apple': 30, 'banana': 12}
flipped: {30: 'apple', 12: 'banana', 50: 'mango'}
squares: [0, 1, 4, 9] | x is still outer
independent rows: [[1, 0, 0], [0, 0, 0]]Real logs arrive as text lines. Three comprehensions pull out the levels, the users who had errors, and a count per day. `sum(1 for ...)` counts how many items pass a test.
Expected output
levels: ['INFO', 'ERROR', 'INFO', 'ERROR', 'WARN']
users with errors: ['asha', 'ravi']
lines per day: {'2026-03-01': 2, '2026-03-02': 3}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Use the loop variable after the comprehension
Print the loop variable of a comprehension afterwards.
squares = [n * n for n in range(3)]
print(n)Break #2
Put else at the end of a filter
Try to write a filter with an else branch.
numbers = [1, 2, 3, 4]
result = [n for n in numbers if n > 2 else 0]Myth vs fact
Myth
A comprehension is always better than a loop.
Fact
It is better when it is short and builds one collection. Long or multi-step logic is clearer as a loop.
Myth
[print(n) for n in numbers] is a neat way to print.
Fact
It builds a list full of None just to throw it away. Use a normal for loop for side effects.
Myth
The loop variable leaks out of a comprehension like in a for loop.
Fact
In Python 3 it does not: a comprehension has its own scope. (In Python 2 list comprehensions leaked.)
Myth
(n * n for n in numbers) is a tuple comprehension.
Fact
There is no tuple comprehension. Round brackets make a generator expression; use tuple(...) if you want a tuple.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
A list comprehension is compiled to tighter bytecode than the equivalent loop with
append(no repeated method lookup), so it is usually somewhat faster. Since Python 3.12 (PEP 709) comprehensions are inlined into the surrounding function instead of creating a hidden function, which makes them cheaper still (the PEP reports up to about twice as fast for tiny comprehensions); the scoping rules stay the same, but a traceback no longer shows a separate<listcomp>frame. Two scope facts still matter: the walrus operator:=inside a comprehension binds its name in the enclosing function ([(total := total + n) for n in nums]updatestotal), and inside a class body a comprehension cannot see the class's own variables except in its firstfor ... inpart. - ▸
A dict comprehension silently drops duplicate keys, the later value winning:
{p: n for n, p in prices.items()}loses entries when two names share a price (the "flipped" example above only works because the prices differ). A comprehension also evaluates its first iterable once, up front, and then walks it; changing the source list from inside the comprehension is as dangerous as changing a list in a loop (Topic 4.9). - ▸
A comprehension builds the entire list in memory. For large or unending data, use a generator expression with
sum,any,all,maxorjoin, which uses constant memory (Phase 9). - ▸
any(x > 3 for x in numbers)stops at the firstTrue, whileany([x > 3 for x in numbers])builds the whole list first. Prefer the generator form inside those functions. - ▸
In data and AI code, comprehensions turn API responses into the shape you need:
[m["content"] for m in messages if m["role"] == "user"]. With NumPy and pandas you will usually replace such loops with whole-array operations (Phase 14), which are much faster.
Remember this
- 1
A list comprehension has the shape
[expression for item in collection]. An expression is any piece of code that produces a value, such asn * n. Read the comprehension like English: "give meexpression, for eachitemincollection".[n * n for n in [1, 2, 3]]is[1, 4, 9]. It is exactly equivalent to making an empty list, looping, and appending, but shorter and usually a little faster. - 2
Add
ifat the end to filter:[n for n in numbers if n % 2 == 0]keeps only the items for which the condition is true. Items that fail the filter are skipped, not replaced. Do not confuse this with the conditional expressiona if condition else b(Topic 2.2), which picks one of two values and goes at the front (in the expression part):["even" if n % 2 == 0 else "odd" for n in numbers]gives one result for every item. - 3
You can use more than one
for, which behaves like nested loops, and the firstforis the outer loop:[(x, y) for x in [1, 2, 3] for y in "ab"]makes six pairs. A common use is flattening a list of lists:[v for row in matrix for v in row]. The rule of thumb: read theforclauses left to right as if they were written as nested loops, top to bottom. - 4
The same idea builds other containers. A dict comprehension uses curly brackets and
key: value:{w: len(w) for w in words}. A set comprehension uses curly brackets with one expression:{w[0] for w in words}. Round brackets do something different:(n * n for n in numbers)is a generator expression, which makes items one at a time instead of building the whole list. You can pass one straight tosum(...); Phase 9 explains generators. - 5
A comprehension has its own scope: the loop variable exists only inside it. After
squares = [n * n for n in range(3)], the namendoes not exist (or, if you had annbefore, it keeps its old value). This is different from a normalforloop, where the loop variable stays alive after the loop ends. - 6
Comprehensions should make code easier to read. If one needs more than one filter, more than two
forclauses, or a long expression, write a normal loop instead. Never use a comprehension just for its side effects, such as printing inside it; that builds a list ofNoneyou throw away.
Explain it without notes
Rewrite a loop that appends squares of even numbers as a comprehension, and explain how to read it.
What is the difference between [n for n in nums if n > 2] and [n if n > 2 else 0 for n in nums]?
How does the order of two for clauses in a comprehension work?
Is the loop variable of a comprehension visible afterwards?
What does funcs = [lambda: i for i in range(3)] give when you call each function, and why?
Practice
Make a list of the cubes of 1 to 5 using a comprehension and range.
Given words = ["sun", "moon", "star", "sky"], build a list of the words that start with "s", in upper case.
Given prices = {"pen": 10, "book": 80, "bag": 500}, build a new dictionary with every price increased by 10 percent (use price * 1.1 and round(..., 2)).
Trade-offs
- ↔
Short and fast, but only while short. A one-line comprehension is easier to read than four lines of loop. Past two clauses it becomes harder than the loop.
- ↔
Builds everything at once. A list comprehension holds the whole result in memory. For huge data, a generator expression trades the ability to index and reuse the result for constant memory.
Done when you can
Done when you can turn a loop that appends into a list comprehension and back.
Done when you can tell a filter
iffrom anif ... elsein the expression.Done when you can write a dict and a set comprehension.
Done when you can read a comprehension with two
forclauses.Done when you can say that the loop variable does not leak and when you would use a loop instead.