You've seen a few list comprehension examples scattered through earlier articles in this series — this one covers the syntax properly, from the basic pattern through nested comprehensions, dict and set variants, and — just as important — when a comprehension is actually the wrong tool for the job.
Introduction: A Concise Way to Build Lists
A python list comprehension is a compact syntax for building a list by applying an expression to every item in an iterable, all in a single line. It replaces a pattern you've likely already written many times: create an empty list, loop over something, append to the list on each iteration.
# Traditional loop
squares = []
for x in range(5):
squares.append(x ** 2)
# Equivalent list comprehension
squares = [x ** 2 for x in range(5)]
print(squares) # [0, 1, 4, 9, 16]Basic syntax
[expression for item in iterable]Read left to right: for each item in iterable, compute expression, and collect all the results into a new list.
A brief historical note
List comprehension syntax was introduced in Python 2.0 through PEP 202, and it draws direct inspiration from set-builder notation in mathematics — the same notation behind writing something like "the set of all x-squared, for x in this range." That mathematical heritage is part of why the syntax reads in an unusual order compared to a typical loop (expression first, then the iteration) — it's mirroring how mathematicians have written this kind of definition for a long time before Python existed.
Filtering and Transforming with Conditionals
Filtering with a trailing if
Adding an if clause at the end of a comprehension filters which items get included — only items where the condition is True make it into the result:
sentence = "the quick brown fox"
vowels = [char for char in sentence if char in "aeiou"]
print(vowels) # ['e', 'u', 'i', 'o']Here, every character is checked against the condition, and only the vowels pass through into the final list.
Conditional expressions for transforming values
This is a genuinely common point of confusion: there's a second, different way if shows up in comprehensions — as a full conditional expression (the ternary operator, covered in the earlier if/elif/else article), placed before the for clause rather than after it. This version doesn't filter anything out — it transforms every item differently based on a condition, but keeps all of them in the result.
numbers = [-5, 3, -2, 8, -1]
n [n if n >= 0 else 0 for n in numbers]
print(non_negative) # [0, 3, 0, 8, 0]Why the position matters
This is worth being explicit about, since the two forms look deceptively similar:
# Filtering — if AFTER the for clause, no else — excludes items
evens = [n for n in range(10) if n % 2 == 0]
# Transforming — if/else BEFORE the for clause — keeps all items, changes some
labeled = [n if n % 2 == 0 else -1 for n in range(10)]The rule of thumb: if at the end, with no else, filters. if/else positioned right after the expression, before for, transforms every item without removing any.
Nested Comprehensions and Dict/Set Variants
Multiple for clauses
Comprehensions can include more than one for clause, functioning like nested loops — genuinely useful for flattening a matrix (a list of lists) into a single flat list:
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
print(flattened) # [1, 2, 3, 4, 5, 6, 7, 8, 9]The for clauses execute left to right, in the exact order they appear — this reads the same order as an equivalent nested loop would run:
# The nested loop equivalent, for comparison
flattened = []
for row in matrix:
for num in row:
flattened.append(num)The first for clause in the comprehension corresponds to the outer loop, and each subsequent for clause corresponds to progressively deeper nested loops.
The same bracket-swap pattern for dicts and sets
The comprehension pattern isn't exclusive to lists — swapping [] for {} (with the right internal syntax) gives you dictionary and set comprehensions, both covered briefly in earlier articles:
# Dictionary comprehension — note the key: value syntax
squares_dict = {n: n ** 2 for n in range(5)}
print(squares_dict) # {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# Set comprehension — same syntax as a list comprehension, but with {}
unique_lengths = {len(word) for word in ["cat", "dog", "fish", "ox"]}
print(unique_lengths) # {3, 4, 2} — duplicates (two 3-letter words) automatically collapsedPractical examples
Inverting a dictionary:
original = {"a": 1, "b": 2, "c": 3}
inverted = {value: key for key, value in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}Building a word-length lookup:
words = ["cat", "elephant", "dog", "hippopotamus"]
lengths = {word: len(word) for word in words}
print(lengths) # {'cat': 3, 'elephant': 8, 'dog': 3, 'hippopotamus': 12}Deduplicating with a set comprehension:
numbers = [1, 2, 2, 3, 3, 3, 4]
unique = {n for n in numbers}
print(unique) # {1, 2, 3, 4}(In that last example specifically, set(numbers) would be simpler and equally effective — the comprehension form earns its place once you need to transform each item while deduplicating, not just deduplicate as-is.)
Performance and Readability Tradeoffs
Why comprehensions are typically faster
List comprehensions are generally faster than the equivalent explicit loop with repeated .append() calls. At the bytecode level, Python optimizes comprehensions using a specialized internal operation for building the list, rather than repeatedly looking up and calling the .append() method on every single iteration the way a manual loop does. For small lists this difference is negligible, but it can add up meaningfully over large datasets or tight loops.
The readability litmus test
Speed isn't the only consideration, and it's often not even the deciding one. A good practical test: if a comprehension takes more than a couple of seconds to mentally parse, it's a sign to refactor it into a full loop or a dedicated helper function instead. Comprehensions are meant to make code more readable, not less — the moment that stops being true for a particular piece of logic, the comprehension has outlived its usefulness there.
# Getting hard to parse at a glance
result = [x * 2 for x in [y ** 2 for y in range(10) if y % 2 == 0] if x > 10]
# Clearer, even though it's more lines
evens_squared = [y ** 2 for y in range(10) if y % 2 == 0]
result = [x * 2 for x in evens_squared if x > 10]Both produce the same output, but splitting the logic into two named steps makes it dramatically easier to follow what each part is actually doing.
Style guide rule of thumb
Several widely used Python style guides — Google's internal style guide among them — recommend keeping each part of a comprehension (the expression, the for clause, and any filter) to roughly one line each, and generally avoiding stacking more than one for or if clause in a single comprehension. Once you're nesting multiple loops and multiple conditions together, that's usually the signal to break the logic apart, exactly as shown above.
When to Use a Loop (or Generator) Instead
Skip comprehensions for side effects
Comprehensions exist to build a collection — they're not meant to be used purely for their side effects, like printing or writing to a file. Using one this way technically works but reads as a misuse of the syntax, and often produces a confusing, throwaway result you didn't actually want:
# Misuse — building a list of None values just to trigger print() as a side effect
[print(x) for x in range(5)] # works, but wasteful and unclear
# Correct — a plain loop, since there's no list being built for later use
for x in range(5):
print(x)The same applies to complex, multi-step logic, or anything requiring a try/except block inside the loop body — comprehensions don't support exception handling gracefully, and forcing it in tends to produce something far less readable than a straightforward loop.
Generator expressions: comprehensions without building a full list
If you need to iterate over the results just once, and don't need them stored as an actual list in memory, a generator expression — identical syntax, but with parentheses instead of square brackets — is often the better choice:
squares_list = [x ** 2 for x in range(1000000)] # builds the entire list in memory, immediately
squares_gen = (x ** 2 for x in range(1000000)) # produces values one at a time, on demandA generator computes each value only when it's actually asked for, rather than computing and storing everything up front. For a huge dataset, or any situation where you're just going to loop through the results once (say, feeding them into sum() or another loop), this can be a significant memory saving over building the full list first.
Quick decision checklist
Need an actual list, the logic is simple, and the dataset is a reasonable size → list comprehension.
Need a dict or set built from an expression → dict/set comprehension.
Involves side effects, complex multi-step logic, or exception handling → a regular loop.
Iterating a huge dataset, or only need to pass through the results once → a generator expression.