A list comprehension is a concise expression that builds a list in a
single readable line: [expression for item in iterable]. It replaces the
common pattern of creating an empty list and append-ing in a for loop.
# the verbose way
squares = []
for n in range(5):
squares.append(n * n)
# the comprehension
squares = [n * n for n in range(5)] # [0, 1, 4, 9, 16]
Beyond brevity, comprehensions are usually faster than an equivalent loop (the iteration runs in optimized C) and they signal intent: "I'm transforming a sequence into a new list." Reach for one whenever you're building a list by mapping or filtering another iterable.
A trailing if clause filters items — only elements for which it is
truthy are kept. A conditional expression (x if cond else y) goes at
the front, before the for, because it's part of the output expression,
not a filter.
nums = range(6)
evens = [n for n in nums if n % 2 == 0] # filter: [0, 2, 4]
labels = ["even" if n % 2 == 0 else "odd" # transform every item
for n in nums] # ['even','odd',...]
both = [n * 2 for n in nums if n > 2] # filter THEN transform
Remember the position rule: a filter if comes after the for; a
if/else expression comes before it. Mixing them up is a common
beginner error.
You can chain multiple for clauses to flatten or iterate over nested
data. The clauses read left to right in the same order as nested
loops. You can also nest a comprehension inside the output expression to
build a list of lists.
matrix = [[1, 2], [3, 4]]
flat = [x for row in matrix for x in row] # [1, 2, 3, 4]
# equivalent to:
# for row in matrix:
# for x in row:
grid = [[r * c for c in range(3)] for r in range(3)] # list of rows
The trap is reading the order backwards — the outer loop is written first. Keep nesting shallow; two levels is usually the readability limit before a plain loop is clearer.
The same syntax works for dicts and sets. A dict comprehension uses
{key: value for ...} and a set comprehension uses {expr for ...}
(no colon). Sets automatically deduplicate results.
names = ["ada", "grace", "ada"]
lengths = {n: len(n) for n in names} # {'ada': 3, 'grace': 5}
unique = {n for n in names} # {'ada', 'grace'} (deduped)
swapped = {v: k for k, v in lengths.items()} # invert a dict
Note {} alone is an empty dict, not a set — use set() for an empty
set. Dict and set comprehensions accept the same if filters and nested
for clauses as list comprehensions.
Comprehensions are for building a collection. Avoid them when you only
want side effects (printing, writing to a DB) — that abuse hides intent
and builds a throwaway list. Use a plain for loop instead. Also skip them
when the logic is so complex that the line becomes unreadable.
# bad: comprehension purely for side effects
[print(x) for x in items] # builds a useless list of Nones
# good: a loop says "do this for each"
for x in items:
print(x)
Comprehensions overlap with map/filter, but are usually more readable
and avoid lambda. Prefer a generator expression (...) over a list comp
when you only iterate once and don't need to materialize the whole result.
A generator expression uses parentheses (...) and is lazy — it yields items
one at a time instead of building the whole list in memory. Ideal for large or
infinite data and for feeding aggregate functions.
squares_list = [n * n for n in range(1000000)] # builds a big list
squares_gen = (n * n for n in range(1000000)) # lazy, tiny memory
total = sum(n * n for n in range(1000000)) # no intermediate list
next(squares_gen) # 0 — pull one at a time
Rule of thumb: use a genexp when you only iterate once or pass it to sum/max/
any; use a list comp when you need indexing, length, or to reuse the result.
No — in Python 3 a comprehension has its own scope, so its loop variable doesn't exist afterward and won't clobber an outer variable of the same name. (This was not true for list comps in Python 2.)
x = "important"
result = [x for x in range(3)]
print(x) # 'important' — outer x untouched
[i for i in range(3)]
print(i) # NameError — i never leaked
Rule of thumb: comprehension variables are private to the comprehension; you can safely reuse names without side effects.
The walrus := lets you compute a value once, bind it, and reuse it in both
the filter and the output — avoiding a double computation of an expensive call.
data = [" 12 ", "x", " 7 "]
# without walrus: parse twice or use a helper
result = [v for s in data if (v := s.strip()).isdigit()]
# 'v' is reused: filtered on isdigit AND used as the output
result # ['12', '7']
Rule of thumb: use := in a comprehension when you'd otherwise call the same
expensive function in both the if and the output expression.
Dict comprehensions take the same if filter (after the for) and conditional
expressions (in the key or value). This makes inverting, filtering, or remapping a
dict a one-liner.
prices = {"a": 10, "b": 0, "c": 5}
in_stock = {k: v for k, v in prices.items() if v > 0} # drop zero
capped = {k: min(v, 8) for k, v in prices.items()} # transform values
flagged = {k: ("free" if v == 0 else v) for k, v in prices.items()}
Rule of thumb: filter with a trailing if, transform with expressions in the key/
value slots — same rules as list comprehensions.
A comprehension runs its iteration and append logic in optimized C with a
specialized bytecode, avoiding the repeated list.append attribute lookup and
method call a manual loop performs each iteration.
# slower: attribute lookup + bound-method call every iteration
out = []
append = out.append # caching append helps, but still Python-level
for n in range(1000):
append(n * n)
# faster: dedicated LIST_APPEND bytecode, no per-item method lookup
out = [n * n for n in range(1000)]
Rule of thumb: comprehensions win on speed and clarity for building collections; for side effects or complex bodies, a plain loop is the right call.
Multiple for clauses flatten (one combined output list); a nested comprehension
in the output position produces a list of lists. The placement of brackets
decides the shape.
m = [[1, 2], [3, 4]]
flat = [x for row in m for x in row] # [1, 2, 3, 4]
nested = [[x * 10 for x in row] for row in m] # [[10, 20], [30, 40]]
Rule of thumb: chained fors flatten; an inner [...] in the expression slot
preserves structure. Read chained fors top-down like nested loops.
Yes — each for clause can reference variables bound by the clauses to its left,
exactly like nested loops. This lets you generate dependent combinations.
# upper triangle of pairs — j depends on i
pairs = [(i, j) for i in range(3) for j in range(i + 1, 3)]
# [(0, 1), (0, 2), (1, 2)]
# filter can also use earlier variables
[(i, j) for i in range(3) for j in range(3) if i != j]
Rule of thumb: inner clauses see outer ones (left-to-right), so you can build triangular ranges, dependent filters, and Cartesian subsets in one expression.
Without the walrus operator, factor the work into a generator step or helper so the expensive call runs once per item, then filter/transform its result. This keeps both correctness and readability.
# naive: process(x) called twice per item
result = [process(x) for x in data if process(x) is not None]
# better: compute once via an inner generator
processed = (process(x) for x in data)
result = [p for p in processed if p is not None]
Rule of thumb: never repeat an expensive call in both the filter and output — use an
intermediate generator (or :=) to compute it a single time.
It returns an empty collection of the matching type — never an error. An empty
iterable simply yields no items, so you get [], {}, or set().
[x * 2 for x in []] # []
{k: v for k, v in []} # {}
{x for x in ()} # set()
[x for x in range(10) if x > 100] # [] — filter removes everything
Rule of thumb: comprehensions degrade gracefully to empty results on empty/filtered input — no need to guard against empty sources.
When it has multiple for clauses plus filters plus a conditional expression all
on one line, readability collapses. Break it into a loop or a helper function — the
goal is clarity, not minimal line count.
# too dense:
r = [f(x) if g(x) else h(x) for sub in data for x in sub if x and p(x)]
# clearer:
r = []
for sub in data:
for x in sub:
if x and p(x):
r.append(f(x) if g(x) else h(x))
Rule of thumb: if you can't read the comprehension aloud in one breath, expand it to a loop — comprehensions should simplify, not obfuscate.
More Comprehensions & Iteration interview questions
More ways to practice
The self-quiz is live. Join our channel for updates, new content & tech tips.