Python List Comprehensions: A Complete Guide
Introduction
If you have written Python for more than a week, you have probably seen code like this:
squares = [x**2 for x in range(10)]
That one line replaces four lines of a traditional loop. This is called a list comprehension — one of Python’s most distinctive and useful features.
List comprehensions let you build lists concisely, transform data elegantly, and filter items in a single readable expression. But they can also become unreadable if overused.
In this guide, you will learn:
- Basic syntax and how comprehensions map to for loops
- Conditional filtering and transformation
- Nested comprehensions
- Real-world use cases
- Dict and set comprehensions
- Generator expressions
- When not to use comprehensions
All examples are tested on Python 3.12.
The Basic Syntax
Start with a traditional loop:
squares = []
for x in range(10):
squares.append(x ** 2)
print(squares)
The equivalent list comprehension:
squares = [x ** 2 for x in range(10)]
print(squares)
Expected output:
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
The general structure is:
[expression for item in iterable]
- expression — what gets added to the list
- item — the current element from the loop
- iterable — the sequence being looped over
You can always mentally translate a comprehension into its loop equivalent:
result = []
for item in iterable:
result.append(expression)
More Examples
Transforming strings:
words = ["hello", "world", "python"]
upper = [w.upper() for w in words]
print(upper)
Expected output:
['HELLO', 'WORLD', 'PYTHON']
Getting string lengths:
lengths = [len(w) for w in words]
print(lengths)
Expected output:
[5, 5, 6]
Mathematical operations:
doubled = [x * 2 for x in range(5)]
print(doubled)
Expected output:
[0, 2, 4, 6, 8]
Adding Conditions
List comprehensions become more powerful when combined with conditions.
Filtering Values
To keep only even numbers:
evens = [x for x in range(20) if x % 2 == 0]
print(evens)
Expected output:
[0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
The condition appears after the iterable. The general pattern for filtering is:
[x for x in items if condition]
Filtering strings by length:
words = ["apple", "banana", "cherry", "date", "elderberry"]
long_words = [w for w in words if len(w) > 5]
print(long_words)
Expected output:
['banana', 'cherry', 'elderberry']
Conditional Transformation
To transform values based on a condition, use an if-else expression:
labels = ["even" if x % 2 == 0 else "odd" for x in range(6)]
print(labels)
Expected output:
['even', 'odd', 'even', 'odd', 'even', 'odd']
The Syntax Position Matters
This distinction confuses many beginners:
# Filtering — if goes at the end
[x for x in items if condition]
# Conditional transformation — if-else goes in the expression
[a if condition else b for x in items]
The if without an else filters items out of the result. The if-else transforms each item into one of two values. Mixing up their positions causes a SyntaxError.
Nested List Comprehensions
List comprehensions can contain multiple for clauses to handle nested data.
Flattening a 2D List
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
flat = [num for row in matrix for num in row]
print(flat)
Expected output:
[1, 2, 3, 4, 5, 6, 7, 8, 9]
The for clauses follow the same order as nested loops — outer first, inner second:
# Traditional nested loop
result = []
for row in matrix:
for num in row:
result.append(num)
# Equivalent comprehension
result = [num for row in matrix for num in row]
Generating Coordinate Pairs
pairs = [(x, y) for x in range(1, 4) for y in range(1, 4)]
print(pairs)
Expected output:
[(1, 1), (1, 2), (1, 3), (2, 1), (2, 2), (2, 3), (3, 1), (3, 2), (3, 3)]
Real-World Examples
Cleaning Data
raw = [" Alice ", "BOB", "", "carol", None]
clean = [name.strip().title() for name in raw if name]
print(clean)
Expected output:
['Alice', 'Bob', 'Carol']
This single line filters out empty strings and None, strips whitespace, and converts to title case. The equivalent loop version would take six or seven lines.
Extracting Data from a List of Dicts
employees = [
{"name": "Alice", "salary": 75000},
{"name": "Bob", "salary": 82000},
{"name": "Carol", "salary": 90000},
]
names = [e["name"] for e in employees]
print(names)
Expected output:
['Alice', 'Bob', 'Carol']
high_earners = [e["name"] for e in employees if e["salary"] > 80000]
print(high_earners)
Expected output:
['Bob', 'Carol']
This pattern appears constantly in data processing and API response handling. It pairs naturally with the pandas data cleaning workflow for more complex transformations.
Working with File Paths
from pathlib import Path
py_files = [f.name for f in Path(".").iterdir() if f.suffix == ".py"]
print(py_files)
This returns all Python filenames in the current directory in one line.
Dict and Set Comprehensions
The same syntax works with curly braces to create dictionaries and sets.
Dictionary Comprehensions
squares_dict = {x: x**2 for x in range(5)}
print(squares_dict)
Expected output:
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
Reversing a dictionary:
original = {"a": 1, "b": 2, "c": 3}
reversed_dict = {v: k for k, v in original.items()}
print(reversed_dict)
Expected output:
{1: 'a', 2: 'b', 3: 'c'}
The dictionary methods guide covers the full set of dict operations — including .items(), .update(), .setdefault(), and merging — that work alongside comprehensions in real projects.
Set Comprehensions
unique_lengths = {len(w) for w in ["apple", "banana", "cherry", "date"]}
print(unique_lengths)
Expected output:
{4, 5, 6}
Sets automatically remove duplicates, so {len(w) for w in ...} gives you the distinct length values only.
Generator Expressions
Replace the square brackets with parentheses and you get a generator expression instead of a list:
# List comprehension — creates all values immediately
squares_list = [x**2 for x in range(1_000_000)]
# Generator expression — produces values one at a time
squares_gen = (x**2 for x in range(1_000_000))
The list allocates memory for all one million values at once. The generator produces each value only when requested, using a tiny constant amount of memory regardless of size.
Generators are ideal when passing results directly to functions like sum(), max(), any(), or all():
total = sum(x**2 for x in range(1000))
largest = max(x**2 for x in range(1000))
any_large = any(x > 500 for x in range(1000))
When a generator expression is the only argument, the outer parentheses serve double duty — no extra brackets needed. For a deeper look at why generators use so much less memory than lists — including how CPython’s memory allocator handles large objects — see the Python memory management deep dive.
When NOT to Use List Comprehensions
This is where many tutorials stop — but knowing when not to use comprehensions is just as important as knowing how.
Avoid Overly Complex Comprehensions
# Hard to read — avoid this
result = [f(x) for x in data if g(x) and h(x) or j(x)]
# Much clearer as a regular loop
result = []
for x in data:
if g(x) and h(x) or j(x):
result.append(f(x))
The loop version is longer but immediately understandable. Code is read far more often than it is written.
Avoid Side Effects
# Wrong — this builds a useless list of None values
[print(x) for x in range(5)]
# Correct — use a for loop for side effects
for x in range(5):
print(x)
List comprehensions should build collections. Operations with side effects — printing, writing files, modifying external state — belong in regular loops.
Avoid Them During Debugging
Comprehensions compress logic into a single expression, which makes it hard to inspect intermediate values or set breakpoints. When debugging, convert the comprehension back into a loop first — intermediate values become visible and breakpoints work naturally.
The Readability Rule
If the comprehension does not fit on one readable line, use a for loop instead.
Cleverness is not a virtue in production code. A slightly longer loop that any developer can understand in five seconds is worth more than a one-liner that requires careful analysis.
Performance
List comprehensions are generally faster than equivalent loops in CPython. The interpreter has specific optimizations for the comprehension bytecode that avoid some of the overhead of calling list.append() repeatedly.
import timeit
loop_time = timeit.timeit(
"""
result = []
for x in range(1000):
result.append(x**2)
""",
number=10000
)
comp_time = timeit.timeit(
"[x**2 for x in range(1000)]",
number=10000
)
print(f"Loop: {loop_time:.3f}s")
print(f"Comprehension: {comp_time:.3f}s")
Example output:
Loop: 0.85s
Comprehension: 0.58s
The exact numbers vary by machine and Python version, but comprehensions consistently run faster. That said, performance is rarely the reason to choose a comprehension — readability and conciseness are the real benefits.
Wrap-Up
List comprehensions are one of the most practical features in Python. Three things to keep in mind:
[expression for item in iterable if condition]— the filteringifcomes at the end[a if condition else b for item in iterable]— the transformingif-elsegoes in the expression position- If the comprehension does not fit on one readable line, use a regular loop
Used correctly, comprehensions make Python code shorter, clearer, and faster. Used carelessly, they make it harder to read and debug.
For more context on why lists behave the way they do in Python — particularly around copying and mutation — see the guide on mutable vs immutable objects. f-strings — covered in the f-strings guide — are the natural way to format output when working with comprehension results. For questions or future tutorial ideas, get in touch via the Contact page.