Python List Comprehensions Complete Guide (2026) - Syntax, Examples & Performance

List comprehensions separate engineers who write Pythonic code from those who write Python-flavored Java. Modern frameworks like FastAPI and Pydantic use them throughout their internals — if you can't read and write them fluently, you're slowing down every code review.

List Comprehension Syntax & Core Structure

A person reads 'Python for Unix and Linux System Administration' indoors.
Photo by Christina Morillo on Pexels

The Basic Formula & Anatomy

Every list comprehension has three parts: an expression, a for clause, and an optional if filter. In that order, left to right.

# Anatomy: [expression | for item in iterable | if condition]

# Wrong way — verbose loop
squares = []
for x in range(10):
    squares.append(x ** 2)

# Right way — comprehension
squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

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

# Transform + filter in one shot
upper_long = [s.upper() for s in ["alice", "bo", "charlie"] if len(s) > 3]
# ['ALICE', 'CHARLIE']

The expression runs once per item that passes the filter. The filter is optional — skip it when you want every element transformed.

Execution Order & Variable Scope

This trips people up. Evaluation goes right-to-left: the for clause runs first, then the if condition, then the expression. The loop variable does not leak into the enclosing scope — unlike a regular for loop.

# Regular for loop POLLUTES namespace
for x in range(5):
    pass
print(x)  # prints 4 — x is still alive!

# Comprehension is scoped — x doesn't escape
result = [x * 2 for x in range(5)]
# print(x) here would raise NameError in Python 3

# Walrus operator (:=) lets you capture intermediate values
import math
results = [y for x in range(10) if (y := math.sqrt(x)) > 2]
# y is accessible after the comprehension in Python 3.8+
print(y)  # last assigned value of y

Use the walrus operator when you need a computed value in both the filter and the expression — it avoids calling the same function twice.

Practical Patterns & Real-World Use Cases

Filtering, Mapping & Conditional Logic

The most common mistake: using map() and filter() together when a comprehension reads better, or using a comprehension when map() is actually cleaner.

emails = ["alice@example.com", "bad-email", "bob@test.org", "", "x@y"]

# Filter valid emails (basic check)
valid = [e for e in emails if "@" in e and len(e) > 5]
# ['alice@example.com', 'bob@test.org']

# Type casting chain — clean and readable
raw = ["1", "2", "bad", "4", "5"]
numbers = [int(x) for x in raw if x.isdigit()]
# [1, 2, 4, 5]

# When map() is actually cleaner (simple 1:1 transformation, no filter)
doubled = list(map(lambda x: x * 2, range(10)))
# vs.
doubled = [x * 2 for x in range(10)]  # still prefer this — no lambda noise

map() shines when you already have a named function: list(map(str.upper, names)) beats [s.upper() for s in names] for brevity. Otherwise, stick with comprehensions.

Nested List Comprehensions & Cartesian Products

Read nested comprehensions left-to-right, just like nested for loops. The first for is the outer loop.

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

# Matrix transpose
transposed = [[row[i] for row in matrix] for i in range(3)]
# [[1, 4, 7], [2, 5, 8], [3, 6, 9]]

# Cartesian product — test case combinations
sizes = ["S", "M", "L"]
colors = ["red", "blue"]
variants = [(s, c) for s in sizes for c in colors]
# [('S', 'red'), ('S', 'blue'), ('M', 'red'), ...]

# Game grid initialization
grid = [[0 for _ in range(5)] for _ in range(5)]

Two levels of nesting: acceptable. Three levels: extract to a function. Nobody should have to trace four for clauses in a single expression during a code review.

Dictionary & Set Comprehensions

# Dict from list of tuples
pairs = [("a", 1), ("b", 2), ("c", 3)]
lookup = {k: v for k, v in pairs}
# {'a': 1, 'b': 2, 'c': 3}

# Filter dict keys
scores = {"alice": 95, "bob": 42, "charlie": 78}
passing = {k: v for k, v in scores.items() if v >= 60}
# {'alice': 95, 'charlie': 78}

# Deduplication with set comprehension
tags = ["python", "Python", "PYTHON", "java"]
unique = {t.lower() for t in tags}
# {'python', 'java'}

Dict comprehensions are faster than calling dict() on a list of tuples for large datasets — the constructor adds overhead from type checking. See the Python docs on dict views for internals.

Performance Optimization & When NOT to Use Comprehensions

A developer typing code on a laptop with a Python book beside in an office.
Photo by Christina Morillo on Pexels

Benchmarking: Comprehensions vs Loops vs map()

import timeit

# Benchmark setup — 1 million elements
setup = "data = list(range(1_000_000))"

loop_time = timeit.timeit(
    "result = []\nfor x in data:\n    result.append(x * 2)",
    setup=setup, number=10
)
comp_time = timeit.timeit(
    "[x * 2 for x in data]",
    setup=setup, number=10
)
map_time = timeit.timeit(
    "list(map(lambda x: x * 2, data))",
    setup=setup, number=10
)

print(f"Loop:   {loop_time:.3f}s")
print(f"Comp:   {comp_time:.3f}s")
print(f"map():  {map_time:.3f}s")
# Typical output (Python 3.12):
# Loop:   1.842s
# Comp:   0.731s
# map():  0.824s
Method Transformation (1M) With Filter (1M) Memory
for loop ~1.84s ~1.90s Full list
List comprehension ~0.73s ~0.68s Full list
map() + filter() ~0.82s ~0.91s Full list
Generator expression ~0.001s* ~0.001s* O(1)

*Generator builds no list — time is construction only; iteration cost is similar to comprehension.

Readability Trade-Offs & Anti-Patterns

import random

# WRONG: double evaluation trap
# random.random() is called TWICE per element — filter and expression
# get different random values — the logic is broken
bad = [random.random() for _ in range(10) if random.random() >= 0.5]

# RIGHT: use walrus operator to capture once
good = [r for _ in range(10) if (r := random.random()) >= 0.5]

# WRONG: side effects in comprehension
[print(x) for x in range(5)]  # signals "building a list" but throws it away

# RIGHT: just use a loop
for x in range(5):
    print(x)

# WRONG: 3-level nesting — unreadable
result = [z for x in a for y in x for z in y if z > 0]

# RIGHT: extract helper
def flatten_deep(nested):
    for x in nested:
        for y in x:
            yield from (z for z in y if z > 0)
result = list(flatten_deep(a))

Generator Expressions & Memory Efficiency

If you're processing 100M rows of data, a list comprehension allocates all of it in RAM at once. A generator expression doesn't.

import sys

# List comprehension — builds entire list in memory
lst = [x ** 2 for x in range(1_000_000)]
print(sys.getsizeof(lst))  # ~8MB

# Generator — holds almost nothing
gen = (x ** 2 for x in range(1_000_000))
print(sys.getsizeof(gen))  # ~104 bytes

# Perfect for aggregate operations — no list needed
total = sum(x ** 2 for x in range(1_000_000))

# Chaining generators — memory stays flat regardless of pipeline depth
pipeline = (x.strip().lower() for x in open("large_file.txt"))
filtered = (line for line in pipeline if line.startswith("error"))

Check out our post on Python generators and lazy evaluation for deeper coverage of generator pipelines in production.

Advanced Patterns, Debugging & Production Refactoring

A person typing on a laptop with a Python programming book visible, capturing technology and learning.
Photo by Christina Morillo on Pexels

Common Gotchas & Debugging Errors

# GOTCHA 1: Late-binding closure in lambdas
# All lambdas return 4, not 0, 1, 2, 3, 4
fns = [lambda: x for x in range(5)]
print([f() for f in fns])  # [4, 4, 4, 4, 4]

# Fix: capture x with a default argument
fns = [lambda x=x: x for x in range(5)]
print([f() for f in fns])  # [0, 1, 2, 3, 4]

# GOTCHA 2: Chained comparisons avoid double function calls
import math
data = range(100)

# Wrong — expensive_func called twice per item
result = [x for x in data if math.sqrt(x) > 3 and math.sqrt(x) < 8]

# Right — single call via chained comparison
result = [x for x in data if 3 < math.sqrt(x) < 8]

# GOTCHA 3: Type annotations for comprehensions
from typing import List
def get_squares(nums: List[int]) -> List[int]:
    return [x ** 2 for x in nums]

The late-binding closure bug is especially nasty because the list comprehension looks correct and produces no error — it just silently returns wrong values. Always test lambda-in-comprehension patterns explicitly.

Production Refactoring Checklist

Before shipping comprehension-heavy code, run through this list. Also see our Python code review checklist for team workflow tips.

# Can you read it in one pass? If not, refactor.
# Bad: what does this even do at first glance?
result = [f"{k}={v}" for d in configs for k, v in d.items() if v is not None and k not in skip]

# Better: named steps
items = (pair for d in configs for pair in d.items())
filtered = ((k, v) for k, v in items if v is not None and k not in skip)
result = [f"{k}={v}" for k, v in filtered]

The original PEP 202 that introduced list comprehensions is still worth a read — the design intent was readability, not cleverness.

Frequently Asked Questions

Q: Are list comprehensions always faster than for loops?

A: For list-building operations, yes — typically 2–2.5x faster in CPython because the bytecode is optimized and avoids repeated list.append attribute lookups. But if the expression inside is complex enough, the difference narrows. Benchmark your specific case with timeit before assuming speed is the reason to use one.

Q: When should I use a generator expression instead of a list comprehension?

A: Whenever you only need to iterate once and don't need random access, use a generator. The canonical cases are feeding sum(), max(), any(), or all() — there's no reason to build a full list first. For datasets above ~100K elements where memory is a concern, generators are the right default.

Q: Can I use list comprehensions with async code in Python 3.12+?

A: Yes — Python 3.6+ supports async for inside comprehensions: [x async for x in async_generator()]. You can only use these inside an async def function. They're common in FastAPI route handlers that need to transform async query results.

Wrap-up

List comprehensions are fast, readable, and expressive — but only when kept simple. The real skill isn't writing them; it's knowing when to stop: one or two levels of nesting, no side effects, no repeated function calls in conditions. Start by replacing every result = []; for x in ...: result.append() pattern in your current codebase — that's the immediate action item.

Comments

Popular posts from this blog

How to Use Docker for Local Development (Complete Guide 2026)

Node.js Error Handling Best Practices 2026: Complete Guide

CSS Flexbox vs Grid: When to Use Each Layout