Home Tutorials Core Syntax

Core Syntax

List Comprehensions: Build Lists Without the Loop Noise

Pyford Notes July 1, 2026 7 min read
Key points
  • A list comprehension collapses a for loop and optional filter into one expression.
  • The if clause filters items before the expression acts on them.
  • Nesting is legal but quickly hurts readability; prefer a plain loop beyond two levels.
  • Use a generator expression instead when you only need to iterate, not store results.

Why comprehensions exist

A common pattern in any language is: start with a collection, apply some transformation or filter, and put the results into a new collection. In Python, the naive version looks like this:

numbers = [1, 2, 3, 4, 5, 6]
squares = []
for n in numbers:
    squares.append(n * n)

That is four lines and a mutable accumulator variable for what is conceptually a single operation. Python introduced list comprehensions to express this pattern as a single statement that reads almost like English: give me [expression] for each item in collection.

Basic syntax

The bracket syntax mirrors the mathematical set-builder notation:

[expression for item in iterable]

Applied to the squares example:

squares = [n * n for n in numbers]
# [1, 4, 9, 16, 25, 36]

The expression is evaluated once per iteration. The item name is scoped to the comprehension in Python 3, so it does not leak into the surrounding scope. Any iterable works: lists, tuples, strings, ranges, file objects, generator expressions, and anything implementing __iter__.

chars = [c.upper() for c in "hello"]
# ['H', 'E', 'L', 'L', 'O']

pairs = [(x, y) for x in range(3) for y in range(3)]
# all 9 combinations of (0,0) through (2,2)

Filtering with if

An optional if clause placed after the for clause filters which items reach the expression. Items that fail the test are silently dropped:

evens = [n for n in range(20) if n % 2 == 0]
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]

long_words = [w for w in sentence.split() if len(w) > 5]

The if here acts as a guard. It is different from a ternary expression in the expression slot, which runs for every item but produces different values:

# ternary in expression slot: runs for all items
labels = ["even" if n % 2 == 0 else "odd" for n in range(6)]
# ['even', 'odd', 'even', 'odd', 'even', 'odd']

# if in filter slot: drops odd numbers entirely
only_evens = [n for n in range(6) if n % 2 == 0]
# [0, 2, 4]

Nested comprehensions

Flattening a 2D list is a classic use case for a nested comprehension:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [cell for row in matrix for cell in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

Read nested comprehensions left to right: the outermost for is written first, matching the order of a regular nested loop. Avoid going deeper than two levels; beyond that, a conventional loop is almost always easier to follow.

Loop vs comprehension at a glance

SituationLoopComprehension
Simple transformVerbose, fineIdeal
Filter + transformClearConcise
Side effects neededRequiredWrong tool
Deep nesting (3+ levels)ClearerHard to read
Large dataset, no storage neededUse generatorUse generator expression
Complex logic per itemPreferredGets unreadable

When not to use them

Do not use a comprehension for side effects. If the point of the loop is to call a function for its effect rather than to collect values, a comprehension is the wrong shape. A list comprehension that nobody reads is just wasted memory.

# Wrong: side-effect loop dressed as a comprehension
[print(item) for item in data]  # creates a list of Nones

# Correct: just loop
for item in data:
    print(item)

Prefer generator expressions when you do not need a list. If the result feeds directly into sum(), any(), max(), or a loop, use parentheses instead of brackets. The generator version never materialises the full list in memory:

total = sum(n * n for n in range(1_000_000))   # no list built
any_long = any(len(w) > 10 for w in words)     # short-circuits

Split complex comprehensions into named steps. If the expression or the filter condition requires more than one mental step to parse, assign intermediate results to variables and use a regular loop. The interpreter does not care which form you choose; the person reading the code at 11pm does.

Dict and set comprehensions The same pattern extends to other container types. Use curly braces and a key: value pair for a dict comprehension ({k: v for k, v in pairs}), or curly braces with a single expression for a set comprehension ({x * 2 for x in items}).