March 26, 20265 min read

Python List Comprehensions: From Basic to 'Please Stop'

Everything you need to know about Python list comprehensions, including when they help readability and when they destroy it.

python list-comprehensions fundamentals performance
Ad 336x280

List comprehensions are one of Python's best features. They're also one of its most abused. Here's how to use them well.

The Basics

A list comprehension builds a list from an iterable in a single expression. Instead of this:

squares = []
for x in range(10):
    squares.append(x ** 2)

You write:

squares = [x ** 2 for x in range(10)]

Same result, fewer lines, and -- once you're used to reading them -- arguably clearer. The pattern is always [expression for item in iterable].

Adding Conditions

You can filter with an if clause:

even_squares = [x ** 2 for x in range(20) if x % 2 == 0]
# [0, 4, 16, 36, 64, 100, 144, 196, 256, 324]

This replaces the pattern of looping, checking a condition, then appending. The if goes at the end and acts like a filter -- only items that pass get included.

You can also use if/else, but the syntax is different and this trips people up constantly:

# if/else goes BEFORE the for (it's part of the expression)
labels = ["even" if x % 2 == 0 else "odd" for x in range(5)]
# ['even', 'odd', 'even', 'odd', 'even']

# filter-only if goes AFTER the for
evens = [x for x in range(10) if x % 2 == 0]

The rule: if you're filtering (keeping/dropping items), if goes after for. If you're choosing between two values for every item, if/else goes before for.

Real-World Examples

Processing API data:

# Extract active user emails
active_emails = [u["email"] for u in users if u["status"] == "active"]

# Clean up form input
cleaned = [field.strip().lower() for field in raw_fields if field.strip()]

# Build a lookup dictionary (dict comprehension, same syntax)
user_by_id = {u["id"]: u for u in users}

File processing:

# Read non-empty, non-comment lines from a config file
with open("config.txt") as f:
    settings = [line.strip() for line in f if line.strip() and not line.startswith("#")]

Nested Comprehensions

This is where things start getting hairy. You can nest for clauses:

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

The order reads like nested for-loops written top to bottom. The equivalent:

flat = []
for row in matrix:        # first 'for' in the comprehension
    for num in row:       # second 'for' in the comprehension
        flat.append(num)

You can also create nested structures:

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

This one has a comprehension inside a comprehension. The outer one builds rows, the inner one builds each row. It works, but you're pushing the limits of what a human can parse at a glance.

When NOT to Use Them

Here's the part most tutorials skip. List comprehensions are great until they aren't.

Don't use them when the logic is complex. If your comprehension needs multiple conditions, function calls, and nested loops, just write a regular loop. Nobody's going to award you points for cramming everything into one line.

Bad:

result = [transform(x, lookup[x.category]) for x in items if x.is_valid() and x.category in lookup and lookup[x.category].threshold > 0.5]

Better:

result = []
for x in items:
if not x.is_valid():
continue
cat = lookup.get(x.category)
if cat and cat.threshold > 0.5:
result.append(transform(x, cat))

The second version is longer but you can actually debug it. You can add a print statement. You can set a breakpoint. The comprehension version is a black box.

Don't use them for side effects. This is just wrong:
# Don't do this
[print(x) for x in items]

This creates a list of None values that you immediately throw away. Use a regular for loop.

Generator Expressions: The Memory-Friendly Alternative

If you're processing large data and don't need the entire list in memory, swap the brackets for parentheses:

# List comprehension -- builds entire list in memory
total = sum([x ** 2 for x in range(10_000_000)])

# Generator expression -- processes one item at a time
total = sum(x ** 2 for x in range(10_000_000))

The generator version uses almost no memory because it yields values one at a time instead of materializing the whole list. For sum(), any(), all(), min(), max(), and other functions that consume iterables, generators are strictly better.

When you pass a generator as the only argument to a function, you can drop the extra parentheses: sum(x for x in items) instead of sum((x for x in items)).

Performance Notes

List comprehensions are faster than equivalent for-loops with .append(). Not dramatically -- maybe 10-30% -- but consistently. The speed comes from the fact that the comprehension is optimized at the bytecode level; Python doesn't have to look up the append method on every iteration.

# Slower (attribute lookup on every iteration)
result = []
for x in data:
    result.append(x * 2)

# Faster (optimized bytecode)
result = [x * 2 for x in data]

For truly performance-critical code on large datasets, neither option is great -- you'd want NumPy or similar. But for everyday Python, comprehensions give you a small, free speed boost.

Set and Dict Comprehensions

Same syntax, different brackets:

# Set comprehension (unique values)
unique_domains = {email.split("@")[1] for email in email_list}

# Dict comprehension
word_lengths = {word: len(word) for word in sentence.split()}

These follow all the same rules -- you can add if filters, nest loops, all of it.

If you want to practice these patterns hands-on, CodeUp has Python exercises that progress from basic comprehensions through nested and conditional patterns. Writing them yourself is the only way to build the intuition for when a comprehension helps versus when it hurts readability.

Ad 728x90