List, Dict and Set Comprehensions
The Pythonic way to build collections — list, dict, and set comprehensions, nested comprehensions, generator expressions, and when a plain loop is better.
A Comprehension Is a For-Loop, Compressed
A comprehension is not a new concept — it is a compact syntax for a pattern you already know extremely well from Module 06: build an empty collection, loop over something, and add a transformed value to that collection on every iteration. Comprehensions exist because this exact pattern is so common that Python gives it its own dedicated syntax, one that reads, once you are fluent in it, almost like a sentence: "give me x, for every x in this collection."
numbers = [1, 2, 3, 4, 5]
squares = []
for n in numbers:
squares.append(n ** 2)
print(squares) # [1, 4, 9, 16, 25]numbers = [1, 2, 3, 4, 5]
squares = [n ** 2 for n in numbers]
print(squares) # [1, 4, 9, 16, 25]Read the comprehension left to right: [ starts a new list, n ** 2 is the expression computed for every element, for n in numbers is exactly the same loop header you would write in a full for-loop, and ] closes the list. Every comprehension in this module follows this same skeleton — an expression, followed by a for clause, optionally followed by an if clause — just wrapped in different brackets depending on what kind of collection you want back.
[n ** 2 for n in numbers] is append(n ** 2) followed by for n in numbers, with the wrapping brackets telling you what container you end up with. This translation works for every comprehension you will meet in this module.Conditional Comprehensions — Filtering While You Build
Adding an if clause after the for filters which elements make it into the result at all — elements that fail the condition are simply skipped, exactly like an if guard inside a for-loop body that only calls append() conditionally.
numbers = range(1, 21)
evens = []
for n in numbers:
if n % 2 == 0:
evens.append(n)
print(evens) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]numbers = range(1, 21)
evens = [n for n in numbers if n % 2 == 0]
print(evens) # [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]This filtering if comes after the for clause and has no else — it only decides whether an element is included, not what value it contributes. That is a genuinely different job from the conditional expression in Part 03, and mixing the two up is one of the most common early comprehension mistakes.
words = ["apple", "kiwi", "banana", "fig", "cherry"]
# Filter on more than one condition
long_a_words = [w for w in words if len(w) > 4 if w.startswith("a")]
# Equivalent to combining with "and":
long_a_words = [w for w in words if len(w) > 4 and w.startswith("a")]
print(long_a_words) # ['apple']Transforming AND Filtering — Not the Same Thing as Filtering Alone
Sometimes you do not want to drop elements that fail a condition — you want to keep every element, but compute a different value depending on the condition. That calls for the conditional expression (the ternary from Module 05), placed in the expression position at the very start of the comprehension, before the for clause.
numbers = [1, 2, 3, 4, 5]
# FILTERING — result may be shorter than the input
evens_only = [n for n in numbers if n % 2 == 0]
print(evens_only) # [2, 4] — odd numbers are dropped entirely
# TRANSFORMING — result is always the same length as the input
labeled = ["even" if n % 2 == 0 else "odd" for n in numbers]
print(labeled) # ['odd', 'even', 'odd', 'even', 'odd']The position of the if is what changes the meaning entirely. An if after the for clause has no matching else and filters. A ternary x if condition else y before the for clause always has an else and transforms every element without dropping any. This is one of the syntax rules worth memorising deliberately, because reading it quickly at a glance is genuinely easy to get backwards.
numbers = range(1, 11)
# Keep only even numbers, and double the ones over 5
result = [n * 2 if n > 5 else n for n in numbers if n % 2 == 0]
print(result) # [2, 4, 12, 16, 20]
# n=2,4 pass the filter and are unchanged (<=5); n=6,8,10 pass the filter and are doubledBuilding a Dict in One Expression
A dict comprehension follows the exact same shape as a list comprehension, but uses curly braces and produces a key: value pair on each iteration instead of a single value.
names = ["Alice", "Bob", "Carla"]
name_lengths = {}
for name in names:
name_lengths[name] = len(name)
print(name_lengths) # {"Alice": 5, "Bob": 3, "Carla": 5}names = ["Alice", "Bob", "Carla"]
name_lengths = {name: len(name) for name in names}
print(name_lengths) # {"Alice": 5, "Bob": 3, "Carla": 5}Dict comprehensions are genuinely useful for two things you will do constantly: inverting a dict (swapping keys and values), and building a dict from two related lists or from an existing dict's .items(), filtered or transformed along the way — this is exactly the pattern from Module 11's dict methods, expressed more compactly.
prices = {"apple": 1.50, "banana": 0.75, "kiwi": 2.20}
# Invert keys and values
price_to_fruit = {price: fruit for fruit, price in prices.items()}
# Build a new dict containing only items above a threshold
expensive = {fruit: price for fruit, price in prices.items() if price > 1.00}
print(expensive) # {"apple": 1.5, "kiwi": 2.2}Building a Set — Automatic Deduplication, For Free
A set comprehension uses curly braces like a dict comprehension, but without the key: value pairing — just a single expression per element, exactly like a list comprehension. The result automatically deduplicates, inheriting every set property from Module 09.
words = ["Apple", "apple", "BANANA", "banana", "Kiwi"]
unique_lowercase = {w.lower() for w in words}
print(unique_lowercase) # {'apple', 'banana', 'kiwi'} — order not guaranteed, duplicates goneThis is a genuinely common real pattern: normalising a batch of user-submitted or scraped text values (mixed casing, near-duplicates) down to a clean set of unique values in a single line, instead of writing a loop with a manual "have I seen this before" check.
unique_lowercase = set()
for w in words:
unique_lowercase.add(w.lower())
# Correct, but three lines and a mutation step for something the comprehension expresses directly.Nested Comprehensions — And Their Readability Limits
A comprehension can contain more than one for clause, which lets you flatten nested structures or compute a cartesian product in a single expression. The clauses read left to right in the same order you would nest the equivalent for-loops.
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [n for row in matrix for n in row]
print(flat) # [1, 2, 3, 4, 5, 6, 7, 8, 9]
# Equivalent nested loop, for comparison:
flat = []
for row in matrix:
for n in row:
flat.append(n)sizes = ["S", "M", "L"]
colors = ["red", "blue"]
combos = [(size, color) for size in sizes for color in colors]
print(combos)
# [('S', 'red'), ('S', 'blue'), ('M', 'red'), ('M', 'blue'), ('L', 'red'), ('L', 'blue')]A genuinely nested comprehension — a comprehension inside a comprehension
Distinct from multiple for clauses in one comprehension, you can also nest one comprehension entirely inside another — used, for example, to transform every row of a matrix while keeping its row structure, rather than flattening it.
matrix = [[1, 2, 3], [4, 5, 6]]
# Double every value, but keep the row/column shape (not flattened)
doubled = [[n * 2 for n in row] for row in matrix]
print(doubled) # [[2, 4, 6], [8, 10, 12]]
# A real transpose (swap rows and columns)
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]
print(transposed) # [[1, 4], [2, 5], [3, 6]]row and i) and their interaction across two nesting levels, entirely inside one dense expression. It is correct, idiomatic Python — but three levels of nested comprehension, or a comprehension combining nesting and a ternary and a filter, is where most experienced reviewers will ask you to rewrite it as a plain loop. Part 07 covers this trade-off directly.The Most Skipped, Most Important Lesson in This Module
Comprehensions are celebrated as "the Pythonic way" so often that it is easy to walk away thinking more comprehension usage is automatically better code. It is not. A comprehension is a readability tool, and like any tool, it stops helping once it is doing too much at once. The honest rule senior engineers actually apply: if you cannot read a comprehension and understand its full behaviour in a single glance, it should be a loop instead.
# Technically valid. Genuinely hard to read at a glance.
result = [
{"id": u["id"], "active_orders": [o for o in orders if o["user_id"] == u["id"] and o["status"] == "open"]}
for u in users if u["is_active"] and u.get("region") == "west"
]result = []
for u in users:
if not u["is_active"] or u.get("region") != "west":
continue
active_orders = [
o for o in orders
if o["user_id"] == u["id"] and o["status"] == "open"
]
result.append({"id": u["id"], "active_orders": active_orders})Notice the loop version is not "worse Python" — it uses a smaller, perfectly reasonable comprehension for the inner active_orders list, and a guard clause (Module 05) for the filtering, rather than cramming every condition into one nested expression. This is the real skill: knowing when a comprehension is the clean, idiomatic choice for a piece of logic, and switching to a loop the moment a comprehension would need a second sentence to explain.
for clause and, at most, one if — comprehension, without a second thought. Two for clauses used to flatten a simple structure — still fine, still idiomatic. Anything beyond that (nested comprehensions containing their own filters, a ternary combined with a filter, or a comprehension that needs a code comment to explain what it is doing) — write it as a loop. Readability, not brevity, is what "Pythonic" actually means here.Generator Expressions — The Lazy Cousin of the List Comprehension
Swap a list comprehension's square brackets for parentheses, and you get a generator expression — syntactically almost identical, but behaviourally very different. A list comprehension builds the entire list in memory immediately. A generator expression builds nothing up front; it produces values one at a time, lazily, only as something asks for the next one.
squares_list = [n ** 2 for n in range(1_000_000)] # built entirely, right now, in memory
squares_gen = (n ** 2 for n in range(1_000_000)) # nothing computed yet — just a plan
print(type(squares_list)) # <class 'list'>
print(type(squares_gen)) # <class 'generator'>For a million elements, squares_list genuinely allocates memory for a million integers immediately. squares_gen allocates almost nothing — it is a small object that knows how to produce the next value on demand, and produces exactly one value at a time as something consumes it, such as a for loop or a function like sum().
transactions = [120.50, 45.00, 300.25, 15.75]
# No need to build an intermediate list just to sum it —
# sum() consumes the generator expression one value at a time
total_over_50 = sum(t for t in transactions if t > 50)
print(total_over_50) # 420.75
# Parentheses are optional when the generator expression is a function's only argument
total_over_50 = sum((t for t in transactions if t > 50)) # identical, just more parenthesesThis module is only a brief introduction — generator expressions are one specific, narrow application of the much larger idea of generators, built with the yield keyword, which you will cover in full depth in Module 28. For now, the practical rule is simple: if you are about to build a list purely to immediately loop over it once and discard it (like feeding it straight into sum(), max(), or any()), a generator expression does the same job without the wasted memory allocation.
A Code Review at an Austin Analytics Company
An engineer at an Austin marketing-analytics company submits a function that builds a summary report from a batch of ad campaign events — for each active campaign, it needs a list of high-value click events attached.
def build_report(campaigns, events):
report = [
{
"campaign": c["name"],
"high_value_clicks": [e for e in events if e["campaign_id"] == c["id"] and e["type"] == "click" and e["value"] > 10 and c["active"]]
}
for c in campaigns
]
return reportWhat the reviewer flags
Two issues, both traceable directly to earlier parts of this module. First, this crosses the readability ceiling described in Part 07 — a single line packs a nested comprehension, four separate and-chained conditions, and a dict literal, all inside one expression the reviewer has to read twice to trust. Second, and more seriously, it is functionally slow: for every campaign, it re-scans the entire events list from scratch, exactly the repeated-linear-scan performance trap from Module 11's Real World example — with 500 campaigns and 200,000 events, that is 100 million comparisons for a report that should take a fraction of a second.
from collections import defaultdict
def build_report(campaigns, events):
high_value_clicks_by_campaign = defaultdict(list)
for e in events:
if e["type"] == "click" and e["value"] > 10:
high_value_clicks_by_campaign[e["campaign_id"]].append(e)
return [
{"campaign": c["name"], "high_value_clicks": high_value_clicks_by_campaign[c["id"]]}
for c in campaigns
if c["active"]
]The events are grouped once, up front, using the exact defaultdict pattern from Module 11. The final comprehension is now a single for with a single if — well inside the readability heuristic from Part 07 — and does a fast O(1) dict lookup per campaign instead of an O(n) scan. Same output, dramatically faster, and readable at a glance. The lesson the reviewer leaves in the comment: "a comprehension should never contain a full second filtering pass over an unrelated list — group first, comprehend second."
Four Misconceptions About Comprehensions
5 Interview Questions — With Complete Answers
Comprehension Mistakes Beginners Make Constantly
Errors You Will Hit With Comprehensions — And Exactly Why
🎯 Key Takeaways
- ✓A comprehension is a compact for-loop: [expression for item in iterable if condition] builds, filters, and transforms in one line.
- ✓A filtering "if" comes after the for clause and has no else — it can shorten the result. A ternary "if/else" comes before the for clause, in the expression — it transforms every element without dropping any.
- ✓Dict comprehensions ({k: v for ...}) and set comprehensions ({x for ...}) follow the same shape as list comprehensions, just with different brackets and, for dicts, a key:value pair per iteration.
- ✓Multiple for clauses in one comprehension flatten nested structures; a comprehension nested inside another comprehension preserves structure (e.g. transposing a matrix).
- ✓Generator expressions — (x for x in iterable) — produce values lazily one at a time and can only be iterated once, unlike a list comprehension which builds the full result immediately in memory.
- ✓Readability, not brevity, is the actual goal. Once a comprehension needs more than one for clause plus a filter, or mixes a ternary with a filter, rewrite it as a plain loop.
- ✓Never re-scan an unrelated collection inside a comprehension's condition — group data once with a dict or defaultdict first, then write a simple, single-pass comprehension over the grouped result.
- ✓A generator expression is exhausted after one full iteration — attempting to iterate it a second time silently produces nothing, not an error.
What comes next
Module 13 puts dictionaries and comprehensions to work on the shape of data you will actually meet in the real world — lists of dicts, dicts of lists, and the deeply nested JSON structures that come back from every real API.
Module 13 → Nested Data StructuresDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.