Python Comprehensions
Build collections in one readable line
A complete, runnable reference for Python comprehensions. List, dict, set, and generator expressions — plus conditions, nesting, the walrus operator, and performance notes. Every code block includes a numbered line-by-line breakdown.
numbers = [1, 2, 3, 4, 5, 6] squares = [n * n for n in numbers] evens = [n for n in numbers if n % 2 == 0] labels = ["even" if n % 2 == 0 else "odd" for n in numbers] square_map = {n: n * n for n in numbers} unique_mods = {n % 3 for n in numbers} pair_sums = [(a, b, a + b) for a in numbers for b in numbers if a < b] print(squares) print(evens) print(labels) print(square_map) print(unique_mods) print(pair_sums)
📚 What This Lesson Covers
Twelve sections, each with runnable code and a numbered explanation of every line
🎯 Why Comprehensions?
A comprehension is an expression that builds a new collection from an existing iterable
The classic way to build a list in a loop needs a manual accumulator, an explicit append call, and an extra variable that exists only to be filled. A comprehension rewrites all of that into a single expression that reads like a description of the result. Once you're used to them, the comprehension form is not just shorter — it's actually clearer, because the "input → transformation" relationship is visible at a glance.
📝 From for-loop to comprehension
numbers = [1, 2, 3, 4, 5] # The explicit-loop version squares_loop = [] for n in numbers: squares_loop.append(n * n) # The comprehension version squares_comp = [n * n for n in numbers] print(squares_loop) print(squares_comp) print(squares_loop == squares_comp)
- 1A list of five numbers to work with.
- 3–5The explicit loop: create an empty list, iterate, square each value, append. Three lines, one of which is just a setup step.
- 8The comprehension: one expression that produces the same result. Read it as "n * n, for each n in numbers".
- 10–11Both print
[1, 4, 9, 16, 25]. - 12Prints
True— the two lists are equal. The comprehension is a drop-in replacement, not a different result.
The four comprehension types
List: [expr for x in it] — brackets.
Dict: {k: v for x in it} — braces with a colon.
Set: {expr for x in it} — braces, no colon.
Generator expression: (expr for x in it) — parentheses, lazy.
📋 List Comprehensions
The most-used form — wrapped in square brackets, builds a list eagerly
📋 Basic List Comprehension
Syntax: [expression for item in iterable]. The expression is evaluated once per item and the results are collected into a new list.
numbers = [1, 2, 3, 4, 5] doubles = [n * 2 for n in numbers] squares = [n ** 2 for n in numbers] names = ["ana", "bob", "cara"] capitalized = [name.capitalize() for name in names] lengths = [len(name) for name in names] print(doubles) print(squares) print(capitalized) print(lengths)
- 1The input list — five integers.
- 3
[n * 2 for n in numbers]— the expressionn * 2is applied to each element. Result:[2, 4, 6, 8, 10]. - 4
[n ** 2 ...]— squaring. Result:[1, 4, 9, 16, 25]. - 5–6A list of strings. The comprehension calls
.capitalize()on each name. Result:['Ana', 'Bob', 'Cara']. - 7Calls
len()on each string. Result:[3, 3, 4]. - 9–12Print all four results — each is an independent new list.
🔁 List Comprehension over Other Iterables
Any iterable works — strings, tuples, dicts, sets, range(), files, generators. A comprehension doesn't care what's being iterated.
word = "hello" scores = {"Ana": 95, "Bob": 88} values = {1, 2, 3} chars = [c for c in word] upper = [c.upper() for c in word] keys = [k for k in scores] tripled = [v * 3 for v in values] range_sq = [n ** 2 for n in range(5)] print(chars) print(upper) print(keys) print(tripled) print(range_sq)
- 1–3Four different iterables: a string, a dict, and a set.
- 5–6Iterating a string yields characters one at a time.
charsis['h', 'e', 'l', 'l', 'o'];upperis the uppercased version. - 7Iterating a dict yields its keys, so
keysis['Ana', 'Bob']. - 8Iterating a set yields its elements (in undefined order).
tripledmight be[3, 6, 9]or[6, 9, 3]. - 9
range(5)yields 0–4. Result:[0, 1, 4, 9, 16].
📖 Dict Comprehensions
Build dictionaries from any iterable in one expression — {key: value for ...}
📖 Basic Dict Comprehension
Syntax: {key_expr: value_expr for item in iterable}. Both key and value are expressions evaluated per item.
numbers = [1, 2, 3, 4, 5] names = ["ana", "bob", "cara"] square_map = {n: n * n for n in numbers} lengths = {name: len(name) for name in names} by_first = {name[0]: name for name in names} print(square_map) print(lengths) print(by_first)
- 1–2Input lists: five numbers and three names.
- 4
{n: n * n for n in numbers}— the loop variablenappears on both sides. Result:{1: 1, 2: 4, 3: 9, 4: 16, 5: 25}. - 5
{name: len(name) ...}— key is the string, value is its length. Result:{'ana': 3, 'bob': 3, 'cara': 4}. - 6
{name[0]: name ...}— key is the first letter. Result:{'a': 'ana', 'b': 'bob', 'c': 'cara'}. If two names started with the same letter, the later one wins. - 8–10All three prints show dicts.
Counter or explicit length verification).🔄 Transforming and Filtering Dicts
Dict comprehensions shine when you need to invert, filter, or reshape an existing dict.
scores = {"Ana": 95, "Bob": 88, "Cara": 72}
inverted = {score: name for name, score in scores.items()}
passing = {name: score for name, score in scores.items() if score >= 80}
uppercased = {name.upper(): score for name, score in scores.items()}
scaled = {name: score * 1.1 for name, score in scores.items()}
print(inverted)
print(passing)
print(uppercased)
print(scaled)- 1A dict with three entries.
- 3Invert the mapping: keys become values, values become keys. Result:
{95: 'Ana', 88: 'Bob', 72: 'Cara'}. - 4Filter by score — only Ana and Bob pass. Result:
{'Ana': 95, 'Bob': 88}. - 5Uppercase the keys. Result:
{'ANA': 95, 'BOB': 88, 'CARA': 72}. - 6Multiply each score by 1.1. Result: values are 104.50000000000001, 96.8, 79.2 (floating-point precision again).
len(set(scores.values())) == len(scores).🔵 Set Comprehensions
Build sets — duplicates are removed automatically, order is not preserved
🔵 Basic Set Comprehension
Syntax: {expression for item in iterable}. Braces, but no colon — that's what distinguishes it from a dict comprehension.
numbers = [1, 2, 2, 3, 3, 3] words = ["apple", "banana", "avocado", "apple"] unique_numbers = {n for n in numbers} unique_lengths = {len(w) for w in words} unique_firsts = {w[0] for w in words} print(unique_numbers) print(unique_lengths) print(unique_firsts)
- 1A list with duplicates.
- 2A list of words, with "apple" repeated.
- 4
{n for n in numbers}— the duplicates are automatically removed. Result:{1, 2, 3}. - 5
{len(w) for w in words}— different words with the same length produce the same value, and sets deduplicate. Result:{5, 6, 7}(apple=5, banana=6, avocado=7). - 6
{w[0] for w in words}— first letters. Result:{'a', 'b'}.
{normalize(s) for s in raw_strings} gives you unique normalised forms in one pass. For unique values in a list with order preserved, use dict.fromkeys() instead.🧮 Set Comprehensions — Real Use Case
Finding the set of tags used across a collection of documents — a common task in text processing.
articles = [
{"title": "A", "tags": ["python", "web"]},
{"title": "B", "tags": ["python", "ai"]},
{"title": "C", "tags": ["web", "ai", "python"]},
]
all_tags = {tag for article in articles for tag in article["tags"]}
print(sorted(all_tags))- 1–5A list of three article dicts, each with a
tagslist. - 7Nested set comprehension: for each article, iterate over its tags. The set deduplicates automatically. Result:
{'python', 'web', 'ai'}(in some order). - 9
sorted()converts the set to a sorted list for predictable display. Result:['ai', 'python', 'web'].
♾️ Generator Expressions
Same syntax as a list comprehension, but with parentheses — and lazy evaluation
♾️ List vs Generator — Memory and Laziness
A list comprehension builds the whole list in memory; a generator expression yields values on demand. This matters when the input is large or the result is only needed once.
numbers = range(1_000_000) list_version = [n * n for n in numbers] gen_version = (n * n for n in numbers) print(type(list_version).__name__) print(type(gen_version).__name__) print(sum(gen_version)) print(next((n * n for n in range(5))))
- 1A
rangeof one million numbers — cheap to construct because it's lazy. - 3
[...]— a list comprehension that builds one million squared values immediately. Memory: several MB. - 4
(...)— a generator expression. Nothing is computed yet. Memory: negligible. - 6–7Types:
listvsgenerator. - 8
sum(gen_version)— consumes the generator, computing each square on demand. Constant memory. Returns the correct sum without ever building the full list. - 9
next(...)pulls one value from a fresh generator. Result:0(the square of 0, the first value ofrange(5)).
sum(), it's exhausted — list(gen_version) afterwards returns []. If you need to iterate multiple times, use a list comprehension or materialise the generator.🔗 Generator Expressions Inside Function Calls
When the generator expression is the only argument to a function, the parentheses are optional. This is why sum(x * x for x in data) looks like it has only one set of parens.
numbers = [1, 2, 3, 4, 5] total = sum(n * n for n in numbers) evens = any(n % 2 == 0 for n in numbers) all_pos = all(n > 0 for n in numbers) biggest = max(n * 2 for n in numbers) print(total) print(evens) print(all_pos) print(biggest)
- 1A list of five numbers.
- 3
sum(n * n for n in numbers)— no extra parens around the generator;sumreceives the generator expression directly. Result: 1 + 4 + 9 + 16 + 25 =55. - 4
any(...)— returnsTrueas soon as it finds a truthy value, short-circuiting the rest.2is even, so it returns quickly. - 5
all(...)— returnsTrueonly if every value passes. All five are positive. - 6
max(...)— finds the largest doubled value:10.
sum, any, all, max, min are the ideal home for a generator expression — you compute each value once, in one pass, without building an intermediate list. This pattern is sometimes called the "feed-a-generator-to-a-reducer" style.🔀 if/else Conditions
Filter (if after the for) vs transform (if before the for) — two very different behaviours
🔍 Filter with if After the for
An if placed after the for acts as a filter. Items that fail the condition are simply skipped.
numbers = [1, 2, 3, 4, 5, 6] evens = [n for n in numbers if n % 2 == 0] positives = [n for n in [-2, -1, 0, 1, 2] if n > 0] with_b = [w for w in ["banana", "apple", "blueberry"] if "b" in w] print(evens) print(positives) print(with_b)
- 1Numbers 1–6.
- 3
if n % 2 == 0filters for even values. Result:[2, 4, 6]. - 4
if n > 0filters out negatives and zero. Result:[1, 2]. - 5
if "b" in wkeeps only words containing the letter b. Result:['banana', 'blueberry'].
if is evaluated once per item, and items that fail are dropped from the result entirely. The result list can be shorter than the input — even empty.🎨 Ternary if/else Before the for
A ternary expression placed before the for transforms every item — nothing is filtered out. The output length always equals the input length.
numbers = [1, 2, 3, 4, 5] labels = ["even" if n % 2 == 0 else "odd" for n in numbers] clamped = [n if n <= 3 else 3 for n in numbers] print(labels) print(clamped) # Both at once: filter AND transform even_labels = ["even" if n < 4 else "big even" for n in numbers if n % 2 == 0] print(even_labels)
- 1Numbers 1–5.
- 3A ternary
if/elsedecides each value. Result:['odd', 'even', 'odd', 'even', 'odd']— same length as input. - 4Clamp values at 3. Result:
[1, 2, 3, 3, 3]. - 7Combined form: the
ifafterforfilters to evens, then the ternary beforeforchooses "even" or "big even" based on the value. - 8Only evens pass the filter; among them, 2 is labelled "even" and 4 is labelled "big even". Result:
['even', 'big even'].
if after for filters; if/else before for transforms. If you write if before for without an else, Python raises a SyntaxError.🧩 Nested Comprehensions
Multiple for clauses in one comprehension — powerful, but easy to over-use
🔁 Cartesian Product — Two Loops
Two for clauses produce every combination. The order matters: read left-to-right like the nested loops they replace.
letters = ["a", "b"] numbers = [1, 2] pairs = [(l, n) for l in letters for n in numbers] print(pairs) # Equivalent explicit loops result = [] for l in letters: for n in numbers: result.append((l, n)) print(result) print(pairs == result)
- 1–2Two short lists.
- 4The comprehension iterates
lfirst (outer), thenn(inner) — same order as the nested loops below. Result:[('a', 1), ('a', 2), ('b', 1), ('b', 2)]. - 7–10The explicit loops produce the same output. Read the comprehension left-to-right; the outermost
forcomes first. - 12–13
Trueconfirms the comprehension is a drop-in replacement.
🎯 Nested with a Condition on the Inner Loop
A filter if after the inner for applies only to the inner iteration.
numbers = [1, 2, 3, 4] pairs_less = [(a, b) for a in numbers for b in numbers if a < b] print(pairs_less) pairs_same_parity = [(a, b) for a in numbers for b in numbers if (a + b) % 2 == 0 and a <= b] print(pairs_same_parity)
- 1Numbers 1–4.
- 3All ordered pairs
(a, b)wherea < b. Result:[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]. - 7Pairs where the sum is even (same parity) and
a ≤ b. Result includes(1, 1),(1, 3),(2, 2),(2, 4),(3, 3),(4, 4).
for can reference the outer loop variable — a is visible to b's condition. This is what makes "all pairs where a < b" expressible in one line.🗺️ Nested Comprehensions for 2D Data
When the input is already nested (a list of lists), you'll often want one comprehension per level: an outer one for rows, an inner one for cells.
grid = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
]
doubled = [[cell * 2 for cell in row] for row in grid]
flattened = [cell for row in grid for cell in row]
print(doubled)
print(flattened)- 1–5A 3×3 grid as a list of lists.
- 7Two separate comprehensions nested — the outer iterates rows, the inner iterates cells within each row. Result: a new 3×3 grid with every value doubled.
- 8One comprehension with two
forclauses — this flattens the grid to a single list of 9 numbers.
[[...] for row in grid] produces a list of lists (same shape as input); [... for row in grid for cell in row] produces a flat list. Pick the one matching the output you actually want.🦭 Walrus Operator in Comprehensions
Python 3.8+ — name an intermediate result once instead of computing it twice
🦭 Avoiding Double Computation
Without the walrus operator, a filter condition that uses an expensive value often forces you to compute that value twice — once for the condition, once in the expression. := lets you name it once.
def expensive(x): return x ** 2 data = range(10) # Without walrus: expensive() is called twice per item result_old = [expensive(x) for x in data if expensive(x) > 10] # With walrus: expensive() is called once per item result_new = [y for x in data if (y := expensive(x)) > 10] print(result_old) print(result_new)
- 1–2A simple function to simulate an expensive computation.
- 4Some data to iterate over.
- 7
[expensive(x) for x in data if expensive(x) > 10]—expensive(x)runs twice for every item: once in the filter, once in the expression. Wasteful for genuinely expensive operations. - 10
[y for x in data if (y := expensive(x)) > 10]—expensive(x)runs once per item, andyis reused in the expression. Same result, half the work. - 12–13Both lists are identical. The walrus version is semantically equivalent, just more efficient.
y to the comprehension — after the comprehension ends, y is not defined in the enclosing scope. In Python 3.8–3.11, y leaked out; the change was a bug fix, not a feature.⚡ Performance — Why Comprehensions Are Faster
Usually 20–40% faster than manual loops for simple transformations
⏱️ Comprehension vs for-loop
The main speedup comes from two things: comprehensions skip the repeated .append() method lookup, and they run their iteration in a specialised bytecode path.
import timeit numbers = list(range(1000)) loop_time = timeit.timeit( "[].extend([]) or [n * n for n in numbers]", globals={}, setup="numbers = list(range(1000))", number=1000, ) comp_time = timeit.timeit( "[n * n for n in numbers]", setup="numbers = list(range(1000))", number=1000, ) print(f"comprehension: {comp_time:.4f}s") print(f"loop variant: {loop_time:.4f}s")
- 1Imports the standard-library
timeitmodule. - 3Test data — 1000 numbers.
- 5–10
timeit.timeit(...)runs the statement 1000 times and returns the total seconds. Thesetupcreates the data fresh each run. - 12–17The same for the comprehension — typically 20–40% faster for this workload.
- 19–20Print the comparison.
timeit on real data before optimising.📊 Memory — the Bigger Difference
Speed is only half the story. The bigger gain from generator expressions is constant memory — useful when the input is large.
import sys big_list = [n for n in range(100_000)] big_gen = (n for n in range(100_000)) print(f"list: {sys.getsizeof(big_list):,} bytes") print(f"generator: {sys.getsizeof(big_gen):,} bytes")
- 1Imports
systo callgetsizeof. - 3A list of 100,000 numbers — everything held in memory at once.
- 4A generator of 100,000 numbers — nothing computed yet.
- 6–7
sys.getsizeof()returns the memory the object itself occupies. The list is roughly 800 KB; the generator is a few hundred bytes — regardless of how many values it will produce.
sum, any, max, or feeding a file — use a generator expression. When you need to iterate multiple times, index into the result, or pass it to something that expects a list, use a list comprehension.🚫 Anti-patterns — When Not to Use a Comprehension
Comprehensions are elegant, but they have a boundary — past it, a loop is clearer
❌ Side Effects
A comprehension is meant to build a value, not to trigger effects. If the goal is to print, write, or mutate external state, use a loop.
# Don't do this — it builds a useless list of None [print(n) for n in range(3)] # Do this for n in range(3): print(n)
- 2The comprehension runs
print(3)three times and also builds[None, None, None]— a list you didn't want and probably won't use. - 5–6The loop is clearly about triggering side effects. Same output, no wasted list.
[f(x) for x in data] just to see the result is fine.❌ Deep Nesting
Three or more for clauses, or a mix of two if filters and two loops, becomes a puzzle to decode. Write the loop.
# Hard to read result = [(a, b, c) for a in range(3) for b in range(3) for c in range(3) if a != b and b != c] # Easier to read result2 = [] for a in range(3): for b in range(3): for c in range(3): if a != b and b != c: result2.append((a, b, c)) print(result == result2)
- 2A three-deep comprehension with an
andfilter. It fits on one line, but reading it aloud takes real effort — and that's a sign. - 5–10The nested-loop version: longer, but every piece is visible. Indentation makes the structure clear.
- 12Prints
True— the two produce identical results.
❌ Comprehension Inside Comprehension When One Would Do
Stacking comprehensions where a single one plus a flat loop would work is a common source of confusion.
# Hard to read: two comprehensions, one inside the other's iterable matrix = [[1, 2], [3, 4]] result_hard = [[cell * 2 for cell in row] for row in [row + [row[0] * 2] for row in matrix]] # Easier: two clear passes augmented = [row + [row[0] * 2] for row in matrix] result_easy = [[cell * 2 for cell in row] for row in augmented] print(result_hard == result_easy)
- 3Two comprehensions inline — the inner one generates the iterable for the outer one, and both are in the same source line. Correct, but costs a re-read.
- 6–7The same logic in two named steps: augment each row, then double every cell. Longer, but each step has a name and a purpose.
- 9Prints
True— both approaches are equivalent.
🏋️ Practice — Check Your Understanding
Three exercises with solutions. Try each before expanding the answer.
Exercise 1 — Predict the Output
What does each line print? Write your answers before expanding.
nums = [1, 2, 3, 4, 5] print([n * 2 for n in nums if n > 2]) print(["big" if n > 3 else "small" for n in nums]) print({n: n * 2 for n in nums if n % 2 == 0}) print(len([n for n in nums for _ in range(2)]))
Show solution
Line 1: [6, 8, 10] — only 3, 4, 5 pass the filter; each is doubled.
Line 2: ['small', 'small', 'small', 'big', 'big'] — nothing is filtered; every value becomes a label.
Line 3: {2: 4, 4: 8} — only even numbers survive the filter.
Line 4: 10 — the comprehension's expression runs twice per element, so the output list has 10 items.
Exercise 2 — Rewrite as a Comprehension
Rewrite this loop as a list comprehension.
words = ["hello", "world", "python"] result = [] for w in words: if len(w) > 5: result.append(w.upper())
Show solution
result = [w.upper() for w in words if len(w) > 5]
The filter goes after the for (only words longer than 5 characters pass). The transformation w.upper() goes before the for (every surviving word is uppercased). Result: ['PYTHON'] — "hello" and "world" are both 5 characters, so only "python" qualifies.
Exercise 3 — Word Length Map
Given a list of words, build a dict mapping each unique word length to the list of words of that length. Use a comprehension (possibly more than one).
words = ["hi", "hello", "be", "world", "python", "yo"]
Show solution
lengths = {len(w) for w in words}
grouped = {
length: [w for w in words if len(w) == length]
for length in lengths
}
print(grouped)The outer dict comprehension iterates over the unique lengths; the inner list comprehension collects matching words. Result: {2: ['hi', 'be', 'yo'], 5: ['hello', 'world'], 6: ['python']}.
Note: this is a pattern where itertools.groupby or collections.defaultdict can be more efficient — the comprehension form re-scans the whole list once per unique length. For small inputs that's fine; for very large ones, prefer the specialised tools.
❓ Frequently Asked Questions
Common questions about comprehensions, answered directly
Use a comprehension when the goal is to build a new collection from an iterable in a single readable expression. Use an explicit for loop when the body has side effects (printing, appending to multiple lists, writing files), contains complex branching, needs break/continue, or is more than one line of logic. A comprehension should read like a description of the result, not like a mini-program.
Yes, usually 20–40% faster for simple cases. Comprehensions run their loop in a specialised bytecode path that avoids the per-iteration overhead of name lookups and method calls in an explicit for loop. The .append() call in a manual loop is the biggest cost — comprehensions do not pay it. For very small collections the difference is negligible; for large ones it matters.
Yes, but placement matters. A filter if goes after the for: [x for x in nums if x > 0]. A ternary expression goes before the for: [('pos' if x > 0 else 'neg') for x in nums]. You cannot use an if that filters values in the expression position — only the ternary form is allowed there.
A list comprehension [x for x in data] builds the entire list in memory eagerly. A generator expression (x for x in data) with parentheses produces values lazily on demand, using O(1) memory regardless of input size. Use a generator expression for large data or when you only need to iterate once (e.g. passing to sum, any, all, max).
Yes — since Python 3.8. The walrus operator := inside a comprehension lets you name an intermediate result that would otherwise be computed twice: [y for x in data if (y := expensive(x)) > 0]. The name y is scoped to the comprehension in Python 3.12+ — accessible only inside the brackets. In 3.8–3.11, it leaked out; the change was a bug fix.
Rarely. Nested comprehensions [x+y for x in a for y in b] are readable when short and the order of the fors matches the natural reading (outer loop first). Once you need three levels of nesting, or an if that references variables from more than one level, an explicit nested loop is almost always clearer. A good test: if you cannot read the comprehension out loud in one breath, it should be a for loop.
Educational Use Disclaimer
This page is part of Poetic Bytes, a free educational resource for learning Python programming. The code examples are provided for instruction and demonstration. They are not production-audited and should not be used as-is in security-sensitive, financial, or safety-critical systems without independent review. See our full disclaimer and terms of use.
📬 Python, weekly
Short lessons, real code, and ecosystem news — sent about once a week
🗣️ Learner Voices
No comments yet — your voice starts the conversation.