Intermediate Python 📂 Comprehensions · 1 of 1 34 min read

Python List, Dict & Set Comprehensions

Master Python's most Pythonic feature — comprehensions. This tutorial covers list, dict, and set comprehensions from first principles, with side-by-side loop comparisons, filtering vs transforming with if/else, nested patterns, real-world data cleaning examples, performance benchmarks vs for loops and map(), generator expressions for huge datasets, and the golden rules that keep your one-liners readable.

Section 01

The Story That Explains Comprehensions

The Assembly Line vs The Single Chef
Imagine you're running a juice shop and 100 apples are sitting on your counter. You need every apple washed, peeled, sliced, and pressed. You have two choices.

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.

💡
The Core Insight

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.


Section 02

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.

🧠 The Comprehension Blueprint
Output
What each element looks like — an expression, a transformation, or a key:value pair
For
Where the elements come fromfor item in iterable
If
Optional filterif condition, keeps only matching items
Brackets
The container[ ] list, { } set, {k: v} dict
📚
List Comprehension
[expr for x in it]
Wraps in square brackets. Returns a list — ordered, indexable, duplicates allowed. The default choice when you need a sequence of results.
🔑
Dict Comprehension
{k: v for x in it}
Wraps in curly braces with a colon between key and value. Returns a dict. Use when each item produces a mapping.
🎯
Set Comprehension
{expr for x in it}
Curly braces, no colon. Returns a set — unordered, unique elements only. Perfect for deduplication in one line.

Section 03

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)
OUTPUT
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

The Comprehension That Replaces It

# One line. Same result. Clearer intent.
squares = [n ** 2 for n in range(10)]

print(squares)
OUTPUT
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

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)
OUTPUT
[0, 4, 16, 36, 64] ['Alice', 'Bob', 'Charlie']
🔑
Read It Left to Right — In English

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


Section 04

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)
OUTPUT
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16, 5: 25}

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)
OUTPUT
{'London': 'UK', 'Paris': 'France', 'Tokyo': 'Japan'}

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)
OUTPUT
{'apple': 1.2, 'bread': 3.5, 'milk': 2.1}

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)
OUTPUT
{'Alice': 85, 'Bob': 92, 'Charlie': 78}
📈
The Two Colons Rule

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.


Section 05

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)}")
OUTPUT
{'the', 'quick', 'brown', 'fox', 'jumps', 'over', 'lazy', 'dog', 'is'} Distinct word count: 9

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)
OUTPUT
{'pdf', 'csv', 'png', 'md'}
❌ Without Set Comprehension
LineCode
1exts = set()
2for f in files:
3    ext = f.split(".")[-1]
4    exts.add(ext)
4 lines, one accumulator
✅ With Set Comprehension
LineCode
1exts = {f.split(".")[-1]
     for f in files}
  
  
1 expression, zero setup

Section 06

Visual Diagram — How A Comprehension Evaluates

01
Python Reads the Right Side First
The 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.
02
Pull One Item at a Time
Python fetches each item from the iterable one by one. For a list of 1,000 items, this loop body runs 1,000 times — but internally, in C, far faster than a Python-level for loop.
03
Check the Filter (if any)
If an 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.
04
Evaluate the Output Expression
The expression on the left is computed using the current item. For a dict, both the key and value expressions run. The result is a single element ready for the container.
05
Append to the Result Container
The value is appended to the list, added to the set (with duplicate check), or inserted into the dict (overwriting existing keys). Once the iterable is exhausted, the container is returned.

Section 07

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.

🔍
Filter (if at the end)
[x for x in it if cond]
Decides whether to include the item at all. Items that fail the condition are dropped entirely. Use when you want fewer items than the input.
🔄
Transform (if/else on the left)
[a if cond else b for x in it]
Decides what value each item becomes. Every item is included, but their form differs. Use when you want the same number of items, differently shaped.
🎯
Both At Once
[a if c1 else b for x in it if c2]
Filter first (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)
OUTPUT
[2, 4, 6, 8] ['odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even'] ['even', 'odd', 'even', 'odd', 'even']
⚠️
The Position Rule — Memorise This

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.


Section 08

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)
OUTPUT
[1, 2, 3, 4, 5, 6, 7, 8, 9]

Reading Order — The Golden Rule

📑 Reading [n for row in matrix for n in row]
1st
for row in matrix — the outer loop; iterates over each sub-list
2nd
for n in row — the inner loop; iterates over the current row
Out
n — each element, appended to the final flat list

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)
OUTPUT
[1, 2, 3] [2, 4, 6] [3, 6, 9]

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)
OUTPUT
['red-S', 'red-M', 'red-L', 'blue-S', 'blue-M', 'blue-L']
🚨
The 2-Level Ceiling

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.


Section 09

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)
OUTPUT
['Alice', '25', 'alice@example.com'] ['Bob', '30', 'bob@example.com'] ['Charlie', '28', 'charlie@example.com']

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)
OUTPUT
{'the': 3, 'cat': 2, 'sat': 1, 'on': 1, 'mat': 1, 'is': 1, 'fat': 1}

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}")
OUTPUT
gmail.com: ['alice', 'charlie'] yahoo.com: ['bob', 'eve'] outlook.com: ['dave']

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)
OUTPUT
Big paid IDs: [4] Totals: {'paid': 675, 'pending': 250}

Section 10

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")
OUTPUT
For loop: 0.612s Comprehension: 0.348s map + lambda: 0.725s
ApproachSpeedReadabilityWhen to Use
ComprehensionFastestExcellent for simple expressionsAlmost always the default choice
For loop~40–75% slowerBest for complex, multi-step logicWhen each iteration has side effects or many steps
map() + lambdaSlowest of the threeLess readable than comprehensionRarely — mostly avoid unless passing a named function
Why Comprehensions Win

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.


Section 11

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:,}")
OUTPUT
Sum of squares 0-9,999,999: 333333283333335000000
📚 List Comprehension
TraitBehaviour
Brackets[ ]
MemoryStores all items
ReusableYes — iterate many times
Best forSmall/medium data you'll use repeatedly
🌱 Generator Expression
TraitBehaviour
Brackets( )
MemoryOne item at a time
ReusableNo — one-shot iteration
Best forHuge data, streams, single-pass sum/max/any
💡
The Function-Argument Shortcut

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.


Section 12

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.

🚫
Side Effects
Log, save, print, mutate
Comprehensions should produce data, not do things. If each iteration writes to a file, calls an API, or logs — use a for loop. Reading a comprehension shouldn't require thinking about side effects.
🧠
Complex Multi-Step Logic
try/except, multiple assignments
You can't put a 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.
👀
Fails the Squint Test
Reader has to parse for >3 seconds
If a colleague can't parse your comprehension at a glance, it's too clever. Break it up. Short, focused comprehensions beat a single "impressive" three-line monster every time.

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)
⚠️
The One-Line Test

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.


Section 13

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

Section 14

Golden Rules

🔑 Comprehension — Non-Negotiable Rules
1
If you can read it aloud as one English sentence, keep it as a comprehension. If you can't, refactor into a for loop. Clarity is not a nice-to-have — it's the whole reason comprehensions exist.
2
Filter 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.
3
Never use a comprehension for side effects — no 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.
4
Two nested 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.
5
For huge iterables, prefer a generator expression (round brackets instead of square). List comprehensions build everything in RAM — generators produce one item at a time. Memory-safe, and often just as fast for single-pass consumption.
6
Dict comprehensions have exactly one colon — between key and value. That's the only shape difference from a set comprehension. Miss the colon and you build a set of tuples. Add an extra and you get SyntaxError.
7
Comprehensions have their own scope. The loop variable (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.
You have completed Comprehensions. View all sections →