Python Comprehensions
A comprehension is a compact, expressive way to build a new list, dictionary, set, or generator from an existing iterable, all in a single line. Instead of writing a multi-line for loop that creates an empty collection and repeatedly calls .append() or [key] = value, you describe the result you want directly: “give me this expression, for each item in this iterable, if this condition holds.” Comprehensions are one of Python’s most distinctive features because they read almost like plain English while typically running faster than the equivalent explicit loop.
Overview / How it works
Python has four comprehension-style constructs, one for each collection type plus a lazy variant:
- List comprehension —
[expr for item in iterable]— builds alist. - Dict comprehension —
{key: value for item in iterable}— builds adict. - Set comprehension —
{expr for item in iterable}— builds aset(duplicates are automatically discarded). - Generator expression —
(expr for item in iterable)— builds a lazygeneratorobject that produces values one at a time, on demand.
Under the hood, every comprehension is really syntactic sugar for a loop. When Python compiles a list comprehension, it creates a small, hidden function that starts with an empty result container, iterates over the source using the normal iterator protocol (calling iter() once and next() repeatedly), optionally filters items with an if clause, evaluates the expression for each surviving item, and inserts the result into the container. That hidden function has its own local scope — a variable you use as a loop variable inside a comprehension does not leak into the surrounding function, unlike a variable used in a regular for loop. This scoping rule was introduced specifically so comprehensions could be optimized and reasoned about independently of the code around them.
Because the loop body is compiled into tight, specialized bytecode (using an opcode like LIST_APPEND instead of a generic method call to .append), comprehensions usually execute faster than the hand-written loop equivalent, sometimes by 20–40%. They are not just cosmetic — they are a genuine performance idiom in addition to a readability one.
Syntax
The general template, with the optional pieces shown in brackets, is:
[expression for item in iterable if condition]
{key_expr: value_expr for item in iterable if condition}
{expression for item in iterable if condition}
(expression for item in iterable if condition)
| Part | Meaning |
|---|---|
expression |
The value to produce for each surviving item; can be any Python expression, including a function call or a conditional (ternary) expression. |
item |
The loop variable; can be a tuple for unpacking, e.g. for k, v in pairs. |
iterable |
Any iterable: a list, string, range, dict, file, another comprehension, etc. |
if condition |
Optional filter; the expression is only evaluated (and the item only kept) when this is truthy. You can chain multiple for and if clauses. |
Examples
Example 1: A simple filtered list comprehension
numbers = range(10)
even_squares = [n ** 2 for n in numbers if n % 2 == 0]
print(even_squares)
Output:
[0, 4, 16, 36, 64]
This iterates over 0 through 9, keeps only the even values because of the if n % 2 == 0 filter, and squares each survivor with n ** 2. It replaces what would otherwise be a four-line loop with an empty list, an if check, and an .append() call.
Example 2: A dict comprehension built from real text
sentence = "Comprehensions make Python code concise and readable"
words = sentence.split()
word_lengths = {word.lower(): len(word) for word in words if len(word) > 4}
print(word_lengths)
Output:
{'comprehensions': 14, 'python': 6, 'concise': 7, 'readable': 8}
Here the comprehension does two jobs at once: it filters out short words (make, code, and) with if len(word) > 4, and it transforms each surviving word into a key: value pair, lower-casing the key. The resulting dictionary preserves the order the words were encountered in, because Python dictionaries maintain insertion order.
Example 3: Nested comprehensions, a set comprehension, and a conditional expression
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [value for row in matrix for value in row]
print(flattened)
unique_remainders = {value % 3 for value in flattened}
print(unique_remainders)
labels = ["even" if value % 2 == 0 else "odd" for value in flattened]
print(labels)
Output:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
{0, 1, 2}
['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd']
flattened uses two for clauses in one comprehension — read them left to right, exactly like nested loops: for row in matrix is the outer loop, for value in row is the inner loop. unique_remainders is a set comprehension, so duplicate remainders (there are several values that give the same result mod 3) collapse automatically. labels shows a conditional expression (x if condition else y) used as the comprehension’s expression — not to be confused with the trailing if filter, which has no else and simply skips items.
How it works step by step / Under the hood
- Python evaluates the outermost iterable (
matrixin Example 3) once, up front, and callsiter()on it to get an iterator. - The comprehension body runs inside its own temporary scope (implemented as a nested code object/frame in CPython), so loop variables like
rowandvaluedo not overwrite same-named variables outside the comprehension. - For each item pulled via
next(), anyiffilter clauses are checked first; if any is falsy, that item is skipped entirely and the expression is never evaluated for it. - For surviving items, the expression is evaluated and the result is inserted into the target container: appended for a list, added for a set, or stored under a key for a dict.
- When every item has been consumed, the finished container is returned as the comprehension’s value. A generator expression skips this step entirely — instead of building a container, it returns a generator object immediately, and each value is computed lazily, only when something calls
next()on it (for example, via aforloop orsum()).
Common Mistakes
Mistake 1: Using a comprehension purely for its side effects
Wrong:
[print(n) for n in range(5)]
This runs and prints the numbers, but it also builds and immediately discards a list [None, None, None, None, None], since print() always returns None. It wastes memory and confuses readers into thinking the result is used somewhere. If you only care about the side effect, use a plain loop:
for n in range(5):
print(n)
Reserve comprehensions for building a new collection from an expression’s return value, not for triggering actions.
Mistake 2: Stacking too much logic into one comprehension
Wrong (technically valid, but hard to read):
data = [[1, -2, 3], [-4, 5, -6], [7, -8, 9]]
result = [x * 2 if x > 0 else abs(x) for row in data for x in row if x != 0]
print(result)
Two for clauses, an if filter, and a conditional expression crammed onto one line force the reader to mentally parse everything at once. When a comprehension needs more than roughly two clauses or contains nested conditionals inside conditionals, prefer breaking it into a named helper function or a regular loop:
def transform(x: int) -> int:
return x * 2 if x > 0 else abs(x)
data = [[1, -2, 3], [-4, 5, -6], [7, -8, 9]]
result = [transform(x) for row in data for x in row if x != 0]
print(result)
The logic is identical, but naming the transformation makes the comprehension’s intent obvious at a glance.
Best Practices
- Keep comprehensions to one clear transformation; if you need more than two
for/ifclauses, extract a helper function or fall back to an explicit loop. - Prefer a generator expression, e.g.
sum(x * x for x in values), over a list comprehension when you only need to iterate once and don’t need the whole collection in memory — it saves memory on large inputs. - Don’t use a comprehension just to call a function for its side effect (like
printorappendon another list) — that’s what aforloop is for. - Use a set comprehension (
{...}) instead of wrapping a list comprehension inset(...)when you only ever need unique values — it avoids building the intermediate list. - Name the loop variable meaningfully (
user, notx) once the expression or condition gets non-trivial — it costs nothing and helps readability. - Remember dict/set comprehensions require hashable keys/elements; using an unhashable type like a
listas a dict key or set element raisesTypeError. - Follow PEP 8: keep comprehensions short enough to fit on one line (or wrap cleanly across a few lines) — if it doesn’t fit comfortably, it’s a sign to simplify.
Practice Exercises
- Write a list comprehension that produces the cubes (
n ** 3) of every integer from 1 to 20 that is divisible by 3. - Given
names = ["Al", "Grace", "Bo", "Ada", "Linus"], write a dict comprehension mapping each name to its uppercase version, but only for names with more than 2 letters. - Given
groups = [[1, 2, 2], [3, 4], [4, 5, 5, 6]], write one line of code that flattens all the sub-lists and produces a sorted list of the unique values using a nested comprehension combined with a set comprehension.
Summary
- Comprehensions build a list, dict, set, or (lazily) a generator from an iterable in a single, readable expression.
- The general form is
[expression for item in iterable if condition], with braces for dict/set and parentheses for a generator expression. - Comprehensions have their own local scope, so their loop variables never leak into the surrounding code.
- Multiple
forclauses read left to right like nested loops; a trailingiffilters items, whilex if cond else yinside the expression chooses between two output values. - They are usually faster than an equivalent explicit loop because they compile to tighter bytecode.
- Use them for transforming/filtering data into a new collection — not for side effects — and prefer generator expressions when you don’t need the full collection in memory.
