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 a list.
  • Dict comprehension{key: value for item in iterable} — builds a dict.
  • Set comprehension{expr for item in iterable} — builds a set (duplicates are automatically discarded).
  • Generator expression(expr for item in iterable) — builds a lazy generator object 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 (matrix in Example 3) once, up front, and calls iter() 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 row and value do not overwrite same-named variables outside the comprehension.
  • For each item pulled via next(), any if filter 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 a for loop or sum()).

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/if clauses, 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 print or append on another list) — that’s what a for loop is for.
  • Use a set comprehension ({...}) instead of wrapping a list comprehension in set(...) when you only ever need unique values — it avoids building the intermediate list.
  • Name the loop variable meaningfully (user, not x) 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 list as a dict key or set element raises TypeError.
  • 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

  1. Write a list comprehension that produces the cubes (n ** 3) of every integer from 1 to 20 that is divisible by 3.
  2. 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.
  3. 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 for clauses read left to right like nested loops; a trailing if filters items, while x if cond else y inside 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.