Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Beginner+150 XP

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.

45 min August 2026
// Part 01 — What a Comprehension Actually Is

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."

The pattern you already know, from Module 06
numbers = [1, 2, 3, 4, 5]

squares = []
for n in numbers:
    squares.append(n ** 2)

print(squares)   # [1, 4, 9, 16, 25]
The exact same result, as a list comprehension
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.

🎯 Pro Tip
The mental translation that always works: take the full for-loop version, and read the comprehension as "the append expression, then the for-loop header, in that order." [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.
// Part 02 — Filtering With if

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.

The full for-loop version
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]
As a filtering comprehension
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.

Multiple conditions and chained filters
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']
// Part 03 — The Ternary-Inside-Comprehension Pattern

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.

Filtering (if after for) vs transforming (ternary before for)
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.

You can combine both — transform AND filter in one comprehension
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 doubled
// Part 04 — Dict Comprehensions

Building 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.

The full loop version
names = ["Alice", "Bob", "Carla"]

name_lengths = {}
for name in names:
    name_lengths[name] = len(name)

print(name_lengths)   # {"Alice": 5, "Bob": 3, "Carla": 5}
As a dict comprehension
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.

Inverting a dict, and filtering while building one
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}
⚠️ Important
Inverting a dict only produces a correct, lossless result if the original values are unique and hashable (the hashability rule from Module 11 — a dict's values become the inverted dict's keys, so they inherit the same restriction). If two fruits shared the same price, inverting would silently lose one of them — whichever key came last in iteration order would win.
// Part 05 — Set Comprehensions

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.

Set comprehension — deduplicating while transforming
words = ["Apple", "apple", "BANANA", "banana", "Kiwi"]

unique_lowercase = {w.lower() for w in words}
print(unique_lowercase)   # {'apple', 'banana', 'kiwi'} — order not guaranteed, duplicates gone

This 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.

Without a comprehension — the manual version
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.
// Part 06 — Nested Comprehensions

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.

Flattening a list of lists — a genuinely common, genuinely readable case
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)
A cartesian product — every combination of two lists
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.

Transposing a matrix — comprehension inside a comprehension
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]]
⚠️ Important
This is genuinely close to the readability ceiling. The transpose example above requires the reader to track two independent loop variables (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.
// Part 07 — When Comprehensions Hurt Readability

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.

Where a comprehension has clearly gone too far
# 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"
]
The same logic, as a loop — slower to write, faster to read
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.

🎯 Pro Tip
A practical heuristic: one 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.
// Part 08 — Generator Expressions

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.

List comprehension vs generator expression — syntax difference
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().

A genuinely common, genuinely idiomatic use — feeding a function that just needs to iterate once
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 parentheses

This 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.

// Part 09 — Real World
💼 What This Looks Like at Work

A Code Review at an Austin Analytics Company

Scenario — Analytics company, Austin · Pull request review

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.

The original submission
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 report

What 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.

The revised version — grouped once, then a clean comprehension per campaign
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."

// Part 10 — Misconceptions

Four Misconceptions About Comprehensions

✕ ""Comprehensions are always faster than a for-loop, so use them everywhere""
Comprehensions are often somewhat faster than an equivalent for-loop with explicit .append() calls, due to internal optimisations — but the difference is rarely the deciding factor, and a badly-nested comprehension over the wrong data structure (as in the Real World example above) can be catastrophically slower than a well-structured loop. Readability, not raw speed, should be the primary reason you reach for one.
✕ ""More comprehension usage is automatically more Pythonic code""
Pythonic means readable and idiomatic, not maximally compressed. A comprehension packed with nested loops, multiple conditions, and a ternary is technically valid but is exactly the kind of code real reviewers push back on. A well-placed plain loop is often the more Pythonic choice.
✕ ""[x if cond else y for ...] and [x for ... if cond] are basically interchangeable""
They do different jobs. The ternary form (if/else before the for) transforms every element and always returns a result the same length as the input. The filtering form (if after the for, no else) can drop elements and may return a shorter result. Mixing them up is a real source of bugs, not just a style issue.
✕ ""A generator expression and a list comprehension always give you the same thing, just written differently""
They behave fundamentally differently, not just syntactically. A list comprehension builds the full result immediately in memory. A generator expression produces values lazily, one at a time, and can only be iterated through once — after that, it is exhausted and produces nothing further, unlike a list, which can be iterated repeatedly.
// Part 11 — Interview Prep

5 Interview Questions — With Complete Answers

What is the general form of a list comprehension, and how does it map to a for-loop?
[expression for item in iterable if condition] — it creates an empty list, loops over the iterable, optionally skips items that fail the condition, and appends the expression's result for every item that remains. It is the exact same behaviour as writing result = []; for item in iterable: if condition: result.append(expression) — just compressed into a single expression.
What is the difference between a filtering if and a ternary if/else inside a comprehension?
A filtering if comes after the for clause and has no else — it decides whether an element is included at all, so the result can be shorter than the input. A ternary if/else comes before the for clause, in the expression position — it decides what value each element becomes, but every element is kept, so the result is always the same length as the input.
How would you flatten a list of lists using a comprehension?
With two for clauses in one comprehension: [n for row in matrix for n in row]. The clauses read left to right in the same order as writing the equivalent nested for-loops — the outer loop (over rows) comes first, the inner loop (over elements within a row) comes second.
What is the difference between a list comprehension and a generator expression?
A list comprehension, written with square brackets, builds the entire result in memory immediately. A generator expression, written with parentheses, produces values lazily one at a time as something consumes them, and can only be iterated through once before it is exhausted. Generator expressions are the better choice when a result will be consumed exactly once — such as passed straight into sum(), max(), or any() — since they avoid allocating memory for an intermediate list that is never fully needed at once.
When would you choose a plain for-loop over a comprehension, even though a comprehension is possible?
When the comprehension would need more than one for clause combined with a filter, or a ternary combined with a filter, or generally when it takes more than a single glance to understand what it produces. Comprehensions are a readability tool; once one becomes dense enough to need re-reading, a loop with named intermediate variables and guard clauses communicates the same logic more clearly, even though it is a few lines longer.
// Common Mistakes

Comprehension Mistakes Beginners Make Constantly

Confusing the filtering if with the ternary if/else
Writing [x if cond for x in items] is a SyntaxError — a ternary always requires an else. If you meant to filter, drop the else and move if after the for: [x for x in items if cond].
Using a comprehension purely for its side effects
[print(x) for x in items] technically works, building a throwaway list of Nones just to trigger print() as a side effect. This is not idiomatic and confuses readers, who expect a comprehension's result to actually be used. Write a plain for loop when the goal is a side effect, not a collection.
Reusing the loop variable name from an outer scope
x = 5; squares = [x for x in range(5)] — inside the comprehension, x is a completely separate variable scoped to the comprehension itself (this changed in Python 3; in Python 2 it leaked into the enclosing scope). The outer x is untouched, but reusing the same name is still confusing to read and worth avoiding.
Iterating a generator expression twice, expecting it to work like a list
gen = (x for x in range(5)); list(gen); list(gen) — the second call returns [] silently, not an error. A generator is exhausted after one full iteration. If you need to iterate more than once, use a list comprehension, or call list() on the generator once and reuse that list.
Nesting comprehensions past the point anyone can read them at a glance
As shown in the Real World example, this is functionally correct but a real code-review liability. Extract the inner logic into a named variable, a helper function, or a grouped lookup built beforehand — do not try to fit every transformation into one expression.
// Error Library

Errors You Will Hit With Comprehensions — And Exactly Why

SyntaxError: invalid syntax (on "[x if cond for x in items]")
Cause: Writing a conditional expression (ternary) inside a comprehension without the required "else" branch.
Fix: If you meant to transform every element conditionally, add an else: [x if cond else default_value for x in items]. If you meant to filter, remove the ternary entirely and move the if after the for clause instead.
NameError: name 'x' is not defined (outside the comprehension)
Cause: Trying to access the comprehension's loop variable after the comprehension has finished, expecting it to leak into the surrounding scope the way a for-loop's variable does.
Fix: Comprehensions have their own scope in Python 3 — the loop variable does not exist outside them. If you need the last value, assign the comprehension's result to a variable and inspect that instead.
TypeError: 'generator' object is not subscriptable
Cause: Trying to index into a generator expression with [0] or a slice, as if it were a list.
Fix: Generators do not support indexing — they only support being iterated once, in order. Wrap it in list(...) first if you need indexing or repeated iteration: list(x for x in items)[0].
KeyError inside a dict comprehension built from .items()
Cause: Filtering or transforming a dict comprehension based on a key that does not exist on every source dict in the iterable being looped over.
Fix: Use .get(key, default) inside the comprehension's expression instead of [key], exactly as covered in Module 11 — {d.get("id", None): d for d in records} rather than {d["id"]: d for d in records} if "id" is not guaranteed to be present.

🎯 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 Structures
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...