List Comprehensions
A list comprehension packs a for-loop that builds a list into a single, readable expression — one of the most distinctively Pythonic tools in the language.
A list comprehension builds a new list by evaluating an expression once for each item an iterable produces, optionally filtering which items are included. The syntax [expression for item in iterable if condition] is equivalent to a for loop that appends to a list, but is typically more concise and, once familiar, more readable — it reads close to the mathematical set-builder notation it's modeled on.
From Loop to Comprehension
Consider the common pattern of building a new list by transforming each element of an existing one. Written as an explicit loop, you create an empty list, then append to it on each iteration. A list comprehension expresses the same operation as a single expression.
numbers = [1, 2, 3, 4, 5]
# Explicit loop
squares = []
for n in numbers:
squares.append(n ** 2)
# Equivalent list comprehension
squares = [n ** 2 for n in numbers]
print(squares) # [1, 4, 9, 16, 25]Anatomy of a Comprehension
A comprehension has three parts, always in this order: the expression (what to compute for each item, placed first), the for clause (which iterable to draw items from, and what name to bind each item to), and an optional filtering if clause (which items to skip).
[ expression for item in iterable if condition ]
^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^
what to where items come from which items to keep
compute (this part is required) (this part is optional)words = ["apple", "kiwi", "banana", "fig"]
long_upper = [w.upper() for w in words if len(w) > 4]
print(long_upper) # ['APPLE', 'BANANA']The Expression Can Be Anything
The expression is not limited to a simple transformation of item — it can be any valid Python expression, including function calls, method calls, conditional expressions, or even a literal tuple to build pairs.
The if/else conditional expression here (x if cond else y) is a different construct from the filtering if at the end of a comprehension — the conditional expression is part of the *expression* and decides what value to produce, while the filtering if decides whether to produce a value at all.
names = ["ada", "grace", "linus"]
titled = [name.title() for name in names]
# a conditional expression inside the comprehension expression
labels = ["even" if n % 2 == 0 else "odd" for n in range(5)]
print(labels) # ['even', 'odd', 'even', 'odd', 'even']
# building tuples
pairs = [(n, n ** 2) for n in range(4)]
print(pairs) # [(0, 0), (1, 1), (2, 4), (3, 9)]Filtering with if
A trailing if condition filters which items from the iterable are processed at all — items that don't satisfy it are skipped entirely, contributing nothing to the result (not even a placeholder).
numbers = range(20)
multiples_of_3 = [n for n in numbers if n % 3 == 0]
print(multiples_of_3) # [0, 3, 6, 9, 12, 15, 18]
# Multiple conditions combine with 'and'/'or' as normal boolean expressions
filtered = [n for n in numbers if n % 2 == 0 and n % 3 == 0]
print(filtered) # [0, 6, 12, 18]Nested Comprehensions: Flattening
A comprehension can include more than one for clause, which nests loops in the same left-to-right order they'd appear as explicit nested for statements. This is most commonly used to flatten a list of lists.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
# Explicit nested loop
flat = []
for row in matrix:
for value in row:
flat.append(value)
# Equivalent nested comprehension -- clauses read left to right, outer first
flat = [value for row in matrix for value in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]Comprehensions of Comprehensions: Building a Matrix
Distinct from a nested (multi-for) comprehension is a comprehension whose *expression* is itself another comprehension — useful for building nested structures, like a 2D grid, rather than flattening one.
# A 3x3 identity-like grid, built with a comprehension inside a comprehension
grid = [[1 if row == col else 0 for col in range(3)] for row in range(3)]
for row in grid:
print(row)
# [1, 0, 0]
# [0, 1, 0]
# [0, 0, 1]Scoping: Comprehensions Have Their Own Namespace
Since Python 3, a comprehension's loop variable is local to the comprehension — it does not leak into the enclosing scope the way a variable in an explicit for loop would. This avoids accidentally overwriting a variable that happens to share the loop variable's name.
n = "outer value"
result = [n for n in range(3)]
print(result) # [0, 1, 2]
print(n) # 'outer value' -- unchanged; the comprehension's n was localReadability: When to Prefer a Loop Instead
Comprehensions shine when the logic is a single, clear transformation and/or filter. Once a comprehension needs more than one for/if clause, or the expression itself is a nontrivial multi-step computation, an explicit loop is usually more readable — a comprehension that requires the reader to mentally 'unnest' several clauses defeats its own purpose. There is no hard rule; if you find yourself squinting at your own comprehension, that's the signal to rewrite it as a loop.
# Getting hard to read -- two for clauses, one if clause, nontrivial expression
result = [x * y for x in range(10) for y in range(10) if x != y if (x + y) % 2 == 0]
# Often clearer as an explicit loop, even though it's longer
result = []
for x in range(10):
for y in range(10):
if x != y and (x + y) % 2 == 0:
result.append(x * y)Performance Note
List comprehensions are typically somewhat faster than the equivalent explicit loop with .append() calls (CPython implementation detail: the comprehension compiles to specialized bytecode that avoids repeated attribute lookups for .append and repeated Python-level loop overhead), but this should be treated as a secondary benefit — readability, not micro-optimization, is the primary reason to reach for a comprehension.
Complete Example: Processing a Dataset
students = [
{"name": "Ada", "score": 97},
{"name": "Grace", "score": 55},
{"name": "Linus", "score": 88},
]
# names of students who passed (score >= 60), uppercased
passed = [s["name"].upper() for s in students if s["score"] >= 60]
print(passed) # ['ADA', 'LINUS']
# a list of (name, letter grade) pairs for everyone
def letter_grade(score: int) -> str:
return "A" if score >= 90 else "B" if score >= 80 else "C" if score >= 60 else "F"
grades = [(s["name"], letter_grade(s["score"])) for s in students]
print(grades) # [('Ada', 'A'), ('Grace', 'F'), ('Linus', 'B')]Key Takeaways
[expression for item in iterable if condition] builds a new list from an existing iterable in one readable expression.
Multiple for clauses nest in left-to-right order, most commonly used to flatten nested iterables.
A conditional expression (x if cond else y) shapes what value is produced; a trailing filtering if decides whether a value is produced at all — these are different constructs.
A comprehension's loop variable is scoped to the comprehension itself and does not leak into the enclosing namespace.
Prefer an explicit loop once a comprehension needs more than roughly one for and one if clause, or the expression becomes hard to read at a glance.
squares = [n 2 for n in numbers] for n in numbers.append(...) # trying to also mutate the source inside the comprehension squares = [n 2 for n in numbers] # perform any needed mutation of numbers separately, before or after
A comprehension is meant to build a new list from a read-only pass over its source iterable; using it to also mutate the source (or another shared structure) as a side effect makes the code hard to follow and is not what comprehensions are for -- use an explicit loop for code with side effects.
result = [x for x in range(10) for y in range(10) if x != y if (x + y) % 2 == 0 for z in range(x)] result = [] for x in range(10): for y in range(10): if x != y and (x + y) % 2 == 0: for z in range(x): result.append(x) # -- or whatever the intended expression is
Stacking several for/if clauses into one comprehension quickly becomes unreadable, even though it's syntactically valid. Once a comprehension needs more than about one for and one if, an explicit nested loop is clearer.
flag = [x > 0 for x in values] if flag: # a non-empty list is always truthy, even [False] if all(x > 0 for x in values):
A list comprehension always produces a list; testing that list's truthiness only checks whether it's non-empty, not what its elements are. Use all()/any() with a generator expression to test a condition across elements.
Rewrite [n for n in range(10) if n % 2 == 0] as an explicit for loop with an if statement.
result = [] for n in range(10): if n % 2 == 0: result.append(n)
In a comprehension, what's the difference between a conditional expression like x if cond else y and a trailing filtering if condition?
The conditional expression is part of what value gets produced for every item -- it always produces something, choosing between x and y. The trailing filtering if decides whether an item is included in the result at all; items that fail it contribute nothing, not even a placeholder value.
Does the loop variable in a list comprehension leak into the surrounding scope in Python 3? Why does this matter?
No -- since Python 3, a comprehension has its own local scope, so its loop variable does not overwrite or leak into a variable of the same name outside it. This matters because it prevents accidental clobbering of an outer variable that happens to share the loop variable's name, a bug that existed in Python 2.
What does [x for row in matrix for x in row] do, given matrix = [[1,2],[3,4]]?
It flattens the nested list into [1, 2, 3, 4]. The for clauses run in left-to-right order matching explicit nested for loops: 'for row in matrix' is the outer loop, 'for x in row' is the inner loop.
When would you choose an explicit for loop over a list comprehension, even though the comprehension would be shorter?
When the comprehension would need more than roughly one for clause and one if clause, or when the expression involves multiple steps/side effects that make a single-expression form hard to read -- readability, not brevity, should decide.
Why are list comprehensions typically somewhat faster than the equivalent explicit loop using .append()?
As a CPython implementation detail, a comprehension compiles to specialized bytecode that avoids the repeated attribute lookup for .append and some of the general per-iteration overhead of an explicit Python-level for loop. This is a secondary benefit though -- readability is the main reason to use comprehensions.
What does [(x, y) for x in range(2) for y in range(2)] produce, and in what order?
[(0, 0), (0, 1), (1, 0), (1, 1)] -- the outer for clause (x) varies slowest, and for each x, the inner for clause (y) runs through its full range, matching the order of equivalent nested for loops.
Write a list comprehension that produces a list of the squares of all odd numbers from 1 to 20 (inclusive).
squares_of_odds = [n 2 for n in range(1, 21) if n % 2 == 1] print(squares_of_odds) # [1, 9, 25, 49, 81, 121, 169, 225, 289, 361] range(1, 21) generates 1 through 20, the filtering if n % 2 == 1 keeps only odd numbers, and the expression n 2 squares each one that passes the filter.
What does this code print?
It prints ['hello', 'greetings'] -- the comprehension keeps only words with more than 3 characters, so 'hi' (2) and 'hey' (3) are filtered out, leaving 'hello' (5) and 'greetings' (9).
Write a function flatten(nested) that takes a list of lists and returns a single flattened list, using a nested list comprehension.
def flatten(nested: list[list]) -> list: return [item for sublist in nested for item in sublist] print(flatten([[1, 2], [3], [], [4, 5, 6]])) # [1, 2, 3, 4, 5, 6]
The outer for clause iterates each sublist in nested, and the inner for clause iterates each item within that sublist -- an empty sublist simply contributes no items, which is why [] in the input causes no error and no output.
What does this print, and what operation does it perform?
It prints [[1, 4], [2, 5], [3, 6]] -- this transposes the matrix. The outer comprehension iterates column index i from 0 to 2, and for each i, the inner comprehension collects row[i] across all rows, effectively swapping rows and columns.
words = ["hi", "hello", "hey", "greetings"]
result = [w for w in words if len(w) > 3]
print(result)matrix = [[1, 2, 3], [4, 5, 6]]
transposed = [[row[i] for row in matrix] for i in range(3)]
print(transposed)