Skip to content
C Codeloom
Python

List Comprehensions in Python: A Practical Guide

A practical guide to Python list comprehensions — syntax, filtering, nested forms, dict and set comprehensions, generator expressions, and when a plain loop is clearer.

·10 min read · By Yash Kesharwani
Intermediate 10 min read

What you'll learn

  • The list comprehension syntax in full
  • How to filter with an if clause
  • Nested comprehensions and when to flatten them back into loops
  • Dictionary and set comprehensions
  • Generator expressions and when to prefer them
  • When a regular for loop is the better choice

Prerequisites

A list comprehension builds a list by describing what you want, rather than by appending one element at a time. Once you start reading them naturally, plain for loops with .append will start to feel verbose for simple transformations. But comprehensions are not a universal upgrade — there is a point past which they become harder to read than the loop they replaced. This post covers the form, the family, and the line you should not cross.

The basic form

A list comprehension is a single expression wrapped in square brackets:

squares = [n * n for n in range(1, 6)]
print(squares)    # [1, 4, 9, 16, 25]

Read it left to right: “build a list of n * n for each n in range(1, 6).” The shape is always:

[ <expression>  for <name> in <iterable> ]

The expression is evaluated once per item and its result becomes one element of the new list.

The equivalent plain loop is:

squares = []
for n in range(1, 6):
    squares.append(n * n)

Both produce the same list. The comprehension is shorter, has no mutable accumulator, and reads as a single thought.

Transforming existing data

The expression can be anything — a method call, a function, a formatted string:

names = ["alice", "bob", "carol"]
upper = [name.upper() for name in names]
print(upper)    # ['ALICE', 'BOB', 'CAROL']

lengths = [len(name) for name in names]
print(lengths)  # [5, 3, 5]

greetings = [f"Hello, {name.title()}!" for name in names]
print(greetings)
# ['Hello, Alice!', 'Hello, Bob!', 'Hello, Carol!']

Each output element corresponds one-to-one with an input element. The shape of the input drives the shape of the output.

Filtering with if

An optional if clause at the end keeps only items that satisfy a condition:

numbers = [1, 2, 3, 4, 5, 6, 7, 8]
evens = [n for n in numbers if n % 2 == 0]
print(evens)    # [2, 4, 6, 8]

The shape becomes:

[ <expression>  for <name> in <iterable>  if <condition> ]

You can combine a transformation with a filter — both clauses sit in one comprehension:

squared_evens = [n * n for n in numbers if n % 2 == 0]
print(squared_evens)    # [4, 16, 36, 64]

The order matters when you read it: “for each n in numbers, if it’s even, give me its square.”

Conditional expressions in the result

Do not confuse the trailing if (which filters) with a conditional expression inside the result (which picks a value):

numbers = [-2, -1, 0, 1, 2]

# Filter: drops the negatives
positives = [n for n in numbers if n > 0]
print(positives)    # [1, 2]

# Conditional expression: keeps every element, but transforms negatives
clamped = [n if n >= 0 else 0 for n in numbers]
print(clamped)      # [0, 0, 0, 1, 2]

The if/else inside the expression position is a Python conditional expression — see Python Conditionals. It always produces a value, so the resulting list has the same length as the input.

Nested comprehensions

The for clause can iterate over a nested structure. Reading two for clauses in a single comprehension is the same as reading two nested loops, left to right outer-to-inner:

matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [n for row in matrix for n in row]
print(flat)    # [1, 2, 3, 4, 5, 6, 7, 8, 9]

The equivalent loop:

flat = []
for row in matrix:
    for n in row:
        flat.append(n)

Two-clause flattening is a sweet spot — clear, common, and worth memorising. Three or more clauses start to strain the eye:

# Possible — but hard to read
triples = [(a, b, c)
           for a in range(1, 5)
           for b in range(a, 5)
           for c in range(b, 5)
           if a * a + b * b == c * c]

When you reach this density, prefer the explicit loop. Comprehensions are for expressing, not for compressing.

Building a list of lists

You can also nest a comprehension inside the expression. The result is a list whose elements are themselves lists:

grid = [[r * c for c in range(1, 4)] for r in range(1, 4)]
print(grid)
# [[1, 2, 3], [2, 4, 6], [3, 6, 9]]

Read the inner comprehension first — it produces a row — then the outer one — which builds rows for each r.

Try it yourself. Given words = ["sky", "code", "Python", "is", "fun"], write one comprehension that returns the uppercased form of every word with three or more letters. Confirm you get ['SKY', 'CODE', 'PYTHON', 'FUN'].

Dictionary comprehensions

The same shape with {key: value ...} builds a dictionary:

squares = {n: n * n for n in range(1, 6)}
print(squares)    # {1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

The expression before the for is a key: value pair. Filtering works the same way:

words = ["apple", "kiwi", "banana", "fig"]
short = {w: len(w) for w in words if len(w) <= 4}
print(short)      # {'kiwi': 4, 'fig': 3}

A common dictionary comprehension swaps keys and values:

codes = {"en": "English", "es": "Spanish", "fr": "French"}
inverse = {v: k for k, v in codes.items()}
print(inverse)
# {'English': 'en', 'Spanish': 'es', 'French': 'fr'}

See Python Dictionaries for the underlying container.

Set comprehensions

A set comprehension uses braces with no colon and produces a set, which deduplicates automatically:

text = "the quick brown fox jumps over the lazy dog"
unique_letters = {c for c in text if c.isalpha()}
print(len(unique_letters))    # 26

This is the natural way to express “the set of distinct values that satisfy …”. See Python Sets for what sets are good at.

Generator expressions

If you swap the brackets for parentheses, you get a generator expression:

squares = (n * n for n in range(1, 6))
print(squares)    # <generator object <genexpr> at 0x...>

A generator expression looks like a list comprehension but does not build a list. It yields one value at a time, on demand. This matters when:

  • The sequence is large and you don’t want to materialise the whole thing
  • You only need to consume it once
  • You’re passing it straight into a function like sum, max, min, any, all, or join

The function call form is especially neat — you can omit the extra parentheses:

total = sum(n * n for n in range(1, 1_000_001))
print(total)    # 333333833333500000

If you’d used a list comprehension here, Python would build a one-million-element list in memory before summing it. The generator computes one square at a time and discards it. Same answer, far less memory.

any and all are an even sharper case — they short-circuit as soon as the answer is known:

items = ["alpha", "beta", "", "gamma"]
has_empty = any(s == "" for s in items)
print(has_empty)    # True

any stops at the first truthy result; nothing past that point is evaluated. You get this for free with a generator expression.

We’ll go deeper on the iterator protocol in Generators and Iterators.

Try it yourself. Without building a list, compute the sum of the first one million odd squares using a generator expression. Then confirm the answer matches what you’d get from sum([n * n for n in range(1, 2_000_001, 2)]) — they should be equal.

When NOT to use a comprehension

A comprehension is the right tool when the description fits comfortably on one line and reads like a sentence. It is the wrong tool when:

The expression is non-trivial. A multi-step computation hidden inside a comprehension is harder to debug than the same code as a plain loop with intermediate variables:

# Hard to read
results = [(complex_transform(parse(line.strip())), score(line))
           for line in lines if line.strip() and not line.startswith("#")]

# Easier to read
results = []
for line in lines:
    line = line.strip()
    if not line or line.startswith("#"):
        continue
    parsed = parse(line)
    results.append((complex_transform(parsed), score(line)))

There are side effects. Comprehensions are for producing a list. If you’re calling functions for their effects (printing, writing to disk, updating a database), use a loop. The intent is clearer and the discarded list isn’t a wasted allocation.

You need to break out early. Comprehensions have no break. If you might want to stop partway through, a loop is the right structure — or a generator expression fed to a function that short-circuits like next or any.

Multiple conditions and nested loops together. Three for clauses with two if clauses can technically fit on one line, but no one will thank you for it.

The test is always: would a reader who hasn’t seen this code before understand it in a single pass?

A worked example

A small program that loads numeric values from text lines, drops the blanks and comments, and computes some summary statistics:

raw_lines = [
    "12",
    "# a comment",
    "",
    "  7  ",
    "not-a-number",
    "42",
]

# Parse what we can; ignore the rest.
def to_int_or_none(s: str) -> int | None:
    try:
        return int(s.strip())
    except ValueError:
        return None

values = [v for line in raw_lines
            if line.strip() and not line.lstrip().startswith("#")
            for v in [to_int_or_none(line)]
            if v is not None]

print(values)              # [12, 7, 42]
print(sum(values))         # 61
print(max(values))         # 42

That two-step trick — for v in [to_int_or_none(line)] — names the result so the next if can filter on it. It works, but is exactly the place where a normal loop reads better. Compare:

values = []
for line in raw_lines:
    line = line.strip()
    if not line or line.startswith("#"):
        continue
    v = to_int_or_none(line)
    if v is not None:
        values.append(v)

Same result, much clearer intent. Reach for whichever serves the next reader best.

Recap

You now know:

  • A list comprehension has the shape [expr for name in iterable], with an optional if filter
  • A conditional expression x if cond else y inside the result picks values without filtering
  • Multiple for clauses iterate left to right, outer to inner
  • Dictionary and set comprehensions use {} with a colon or without
  • Generator expressions use () and stream values one at a time
  • Comprehensions are for expressing simple transformations clearly — when they stop being clear, use a loop

Next steps

The next post moves from data transformations to data modelling — defining your own types with classes and objects. Once you can describe a User or an Order with its own attributes and methods, your programs start to look much more like the domain they live in.

→ Next: Classes and Objects in Python

Questions or feedback? Email codeloomdevv@gmail.com.