The Story That Explains Comprehensions
Option A — The old kitchen way: Grab an empty crate. Pick up apple #1, wash it, peel it, slice it, drop it in the crate. Pick up apple #2, repeat. Four full lines of instructions for something conceptually simple.
Option B — The assembly line: "Every apple on this counter, washed and pressed, into that crate — go." One sentence. One instruction. Same result.
That is the difference between a for loop and a comprehension. Same output — one is a recipe, the other is a spec.
A comprehension is Python's way of building a new collection — a list, dict, or set — in a single readable expression from an existing iterable. It replaces the classic "create empty container → loop → append" pattern with one line that reads almost like English.
Comprehensions aren't just shorter — they're faster and they express intent. A for loop tells the reader how to build the collection. A comprehension tells the reader what the collection is. That shift from "how" to "what" is the entire Pythonic mindset.
The Universal Syntax
All three comprehensions — list, dict, and set — share the same skeleton. Learn one shape and you know all three. The only thing that changes is the brackets and what you emit on the left.
List Comprehensions — The Workhorse
List comprehensions are the most common. They shine anywhere you'd otherwise write a for loop that appends to a list.
The Loop You've Always Written
# The old way — 4 lines to square 10 numbers
squares = []
for n in range(10):
squares.append(n ** 2)
print(squares)
The Comprehension That Replaces It
# One line. Same result. Clearer intent.
squares = [n ** 2 for n in range(10)]
print(squares)
Adding a Filter
# Only even numbers, squared
even_squares = [n ** 2 for n in range(10) if n % 2 == 0]
# Cleaning a list of user inputs
raw = [" Alice ", "", "Bob", None, " Charlie"]
clean = [name.strip() for name in raw if name]
print(even_squares)
print(clean)
[n**2 for n in range(10) if n % 2 == 0] reads as:
"n squared, for each n in range 10, where n is even."
If you can read your comprehension out loud like a sentence, it's Pythonic.
If you can't, break it back into a loop.
Dict Comprehensions — Building Mappings
Dict comprehensions are indispensable when you're transforming, inverting, or filtering key-value data. The syntax mirrors list comprehensions but with a key: value expression on the left.
Building a Lookup Table
# Number → its square (classic use case)
square_map = {n: n ** 2 for n in range(6)}
print(square_map)
Inverting a Dict (Keys ↔ Values)
country_capital = {"UK": "London", "France": "Paris", "Japan": "Tokyo"}
# Flip it — capitals become keys
capital_country = {v: k for k, v in country_capital.items()}
print(capital_country)
Filtering a Dict by Value
prices = {"apple": 1.20, "bread": 3.50, "milk": 2.10, "steak": 18.00}
# Keep items under £5
affordable = {item: price for item, price in prices.items() if price < 5}
print(affordable)
Building a Dict from Two Lists
names = ["Alice", "Bob", "Charlie"]
scores = [85, 92, 78]
# zip() pairs them; comprehension builds the dict
report = {name: score for name, score in zip(names, scores)}
print(report)
A dict comprehension has exactly one colon on the left — between key and value. Everything else uses the same shape as a list comprehension. If you find yourself writing two colons or nesting weird brackets, you're overcomplicating it — step back.
Set Comprehensions — Instant Deduplication
Set comprehensions look identical to dict comprehensions but without the colon. They produce a set — an unordered collection with automatic uniqueness. This makes them perfect for anywhere you want distinct results.
Unique Words from a Sentence
text = "the quick brown fox jumps over the lazy dog the fox is quick"
# Unique words, all lowercase
unique_words = {w.lower() for w in text.split()}
print(unique_words)
print(f"Distinct word count: {len(unique_words)}")
Unique File Extensions in a Folder
files = [
"report.pdf", "data.csv", "summary.pdf",
"chart.png", "raw.csv", "notes.md", "logo.png"
]
# Grab the extension of each file, dedupe automatically
extensions = {f.split(".")[-1] for f in files}
print(extensions)
| Line | Code |
|---|---|
| 1 | exts = set() |
| 2 | for f in files: |
| 3 | ext = f.split(".")[-1] |
| 4 | exts.add(ext) |
| 4 lines, one accumulator | |
| Line | Code |
|---|---|
| 1 | exts = {f.split(".")[-1] |
| for f in files} | |
| 1 expression, zero setup | |
Visual Diagram — How A Comprehension Evaluates
for x in iterable clause is parsed first. Python identifies the source of items — the range, list, generator, or dict.items() call — and prepares to iterate over it.if clause exists, the current item is tested. If the condition is False, this item is skipped — the output expression never runs. If True, we move on.Conditionals — Filter vs Transform
There are two very different places an if can appear in
a comprehension. Confusing them is the #1 mistake beginners make.
if c2 at end), then transform survivors
(a if c1 else b on left). Powerful but read it twice before writing it.
Side-by-Side Comparison
nums = [1, 2, 3, 4, 5, 6, 7, 8]
# FILTER: only keep even numbers → shorter output
evens = [n for n in nums if n % 2 == 0]
# TRANSFORM: mark each number as "even" or "odd" → same length
labels = ["even" if n % 2 == 0 else "odd" for n in nums]
# BOTH: label only numbers greater than 3
mixed = ["even" if n % 2 == 0 else "odd"
for n in nums if n > 3]
print(evens)
print(labels)
print(mixed)
If if is after the for, it's a filter (no else allowed).
If if...else is before the for, it's a ternary transform
(else required). Mix them up and Python throws SyntaxError. Get this
right and you'll never fear comprehensions again.
Nested Comprehensions — Flattening & Grids
You can nest multiple for clauses in one comprehension. The rule is
simple: they read left-to-right, top-to-bottom, exactly like nested
for loops.
Flattening a Matrix (List of Lists → List)
matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
# Outer loop first, inner loop second — same as nested for
flat = [n for row in matrix for n in row]
print(flat)
Reading Order — The Golden Rule
[n for row in matrix for n in row]Building a Matrix (Nested List Comprehension)
# A 3x3 multiplication table — list of lists
mult_table = [[i * j for j in range(1, 4)] for i in range(1, 4)]
for row in mult_table:
print(row)
Cartesian Product (All Pairs)
colours = ["red", "blue"]
sizes = ["S", "M", "L"]
# Every colour × every size = 6 T-shirts
skus = [f"{c}-{s}" for c in colours for s in sizes]
print(skus)
Two nested fors in one comprehension is fine. Three is unreadable.
If you're reaching for triple nesting, stop. Split it into a
helper function or a plain for loop. The reader — including future you —
will thank you. Clever code that no one can maintain is a liability, not an asset.
Practical Real-World Examples
Example 1 — Cleaning Messy CSV Data
raw_rows = [
" Alice, 25 , alice@example.com ",
"Bob,30,bob@example.com",
" Charlie , 28,charlie@example.com ",
"", # blank row
]
# Split, strip whitespace, skip blanks — all in one pass
clean = [
[field.strip() for field in row.split(",")]
for row in raw_rows if row.strip()
]
for row in clean:
print(row)
Example 2 — Counting Word Frequencies
text = "the cat sat on the mat the cat is fat"
words = text.split()
# Dict comprehension over a set of unique words
freq = {w: words.count(w) for w in set(words)}
print(freq)
Example 3 — Grouping Users by Domain
emails = ["alice@gmail.com", "bob@yahoo.com",
"charlie@gmail.com", "dave@outlook.com",
"eve@yahoo.com"]
# Get the unique domains
domains = {e.split("@")[1] for e in emails}
# Group users under each domain
grouped = {
d: [e.split("@")[0] for e in emails if e.endswith(d)]
for d in domains
}
for domain, users in grouped.items():
print(f"{domain}: {users}")
Example 4 — Data Transformation Pipeline
transactions = [
{"id": 1, "amount": 100, "status": "paid"},
{"id": 2, "amount": 250, "status": "pending"},
{"id": 3, "amount": 75, "status": "paid"},
{"id": 4, "amount": 500, "status": "paid"},
]
# Extract IDs of paid transactions over £100
big_paid = [t["id"] for t in transactions
if t["status"] == "paid" and t["amount"] > 100]
# Total revenue by status
total_by_status = {
s: sum(t["amount"] for t in transactions if t["status"] == s)
for s in {t["status"] for t in transactions}
}
print("Big paid IDs:", big_paid)
print("Totals: ", total_by_status)
Performance — Are Comprehensions Actually Faster?
Yes — measurably. Comprehensions are optimised bytecode. The interpreter allocates
the target container once and uses specialised instructions to append. A plain for
loop pays the cost of a Python-level list.append method lookup on every
iteration.
import timeit
# Squaring 100,000 numbers — three different ways
setup = "nums = range(100_000)"
t_loop = timeit.timeit(
"result = []\nfor n in nums: result.append(n*n)",
setup=setup, number=100
)
t_comp = timeit.timeit(
"result = [n*n for n in nums]",
setup=setup, number=100
)
t_map = timeit.timeit(
"result = list(map(lambda n: n*n, nums))",
setup=setup, number=100
)
print(f"For loop: {t_loop:.3f}s")
print(f"Comprehension: {t_comp:.3f}s")
print(f"map + lambda: {t_map:.3f}s")
| Approach | Speed | Readability | When to Use |
|---|---|---|---|
| Comprehension | Fastest | Excellent for simple expressions | Almost always the default choice |
| For loop | ~40–75% slower | Best for complex, multi-step logic | When each iteration has side effects or many steps |
| map() + lambda | Slowest of the three | Less readable than comprehension | Rarely — mostly avoid unless passing a named function |
Comprehensions bypass the repeated LOAD_METHOD and
CALL_METHOD bytecode overhead of .append().
The container is built by an internal C-level fast path. The larger the
iterable, the wider the gap grows.
Generator Expressions — The Lazy Cousin
Swap the square brackets for round brackets and you get a generator expression. It looks like a list comprehension but produces values one at a time, on demand — using near-zero memory even for billions of items.
# List comprehension — builds ALL squares in memory at once
squares_list = [n ** 2 for n in range(10_000_000)]
# Uses ~350 MB RAM
# Generator expression — produces one square at a time
squares_gen = (n ** 2 for n in range(10_000_000))
# Uses ~200 bytes
# Both work with sum() — but only the generator is memory-safe
total = sum(n ** 2 for n in range(10_000_000))
print(f"Sum of squares 0–9,999,999: {total:,}")
| Trait | Behaviour |
|---|---|
| Brackets | [ ] |
| Memory | Stores all items |
| Reusable | Yes — iterate many times |
| Best for | Small/medium data you'll use repeatedly |
| Trait | Behaviour |
|---|---|
| Brackets | ( ) |
| Memory | One item at a time |
| Reusable | No — one-shot iteration |
| Best for | Huge data, streams, single-pass sum/max/any |
When a generator expression is the only argument to a function, you can
drop the outer parentheses:
sum(n ** 2 for n in range(1000)) — clean.
sum((n ** 2 for n in range(1000))) — legal but ugly.
When NOT to Use a Comprehension
Comprehensions are powerful, but they aren't a universal replacement for loops.
Reach for a plain for loop when the logic no longer fits comfortably
in one expression.
try/except, multiple statements, or a
break in a comprehension. If any of those come up, the loop
form is not just cleaner — it's the only option.
Bad Idea — Comprehension for Side Effects
# DON'T DO THIS — building a throwaway list just to run print()
[print(user) for user in users]
# Creates [None, None, None, ...] and immediately discards it. Wasteful and confusing.
# DO THIS instead
for user in users:
print(user)
A good comprehension fits on one line, or breaks naturally across 2–3 short lines. If yours wraps past 3 lines or the reader has to trace bracket levels to understand it, refactor. The whole point was clarity — if you've lost that, you've lost the win.
Comparison Table — All Three At a Glance
| Feature | List Comprehension | Dict Comprehension | Set Comprehension |
|---|---|---|---|
| Syntax | [expr for x in it] |
{k: v for x in it} |
{expr for x in it} |
| Returns | list | dict | set |
| Order | Preserved | Insertion order (Py 3.7+) | Not guaranteed |
| Duplicates | Allowed | Keys must be unique (last wins) | Automatically removed |
| Element type | Any | Key must be hashable | Must be hashable |
| Filter allowed | Yes — if cond |
Yes — if cond |
Yes — if cond |
| Best for | General-purpose lists, transforms | Lookup tables, mappings, inversions | Deduplication, membership tests |
Golden Rules
if goes at the end, ternary if/else goes at the front.
Mixing them up is the #1 comprehension bug. Filter drops items; ternary transforms
them. Two totally different jobs.
print,
no file writes, no API calls in the output expression. If you're not collecting the
result, you shouldn't be using a comprehension.
fors is the ceiling. Three or more nested
levels is unreadable — always. Break it into a helper function or use plain loops.
Cleverness that costs readability is technical debt.
SyntaxError.
n in [n*2 for n in nums]) does not leak into
the outer namespace. This is a feature — you can safely reuse variable names
without stomping on outer variables.