🎯 Learning Objectives
- Write list, dict, and set comprehensions fluently
- Add filter conditions with
if - Use inline
if/else(ternary) expressions inside comprehensions - Understand nested comprehensions and when to avoid them
- Use generator expressions for memory-efficient processing
- Know when a plain
forloop is the better choice
Why Comprehensions?
Comprehensions are a concise, readable way to build collections from iterables. They replace the common pattern of creating an empty list, looping, and appending — with a single expressive line.
# ── Traditional loop approach ──
squares = []
for n in range(10):
squares.append(n ** 2)
# ── List comprehension — same result, one line ──
squares = [n ** 2 for n in range(10)]
print(squares)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
why_comprehensions.py
List Comprehensions
Anatomy
# [ expression for item in iterable ]
result = [item * 2 for item in range(5)]
# ^^^^^^^^ ^^^^ ^^^^^^^^
# what to loop source
# produce var iterable
print(result) # [0, 2, 4, 6, 8]
anatomy.py
Common transformations
words = ["hello", "world", "python"]
# Uppercase all words
upper = [w.upper() for w in words]
print(upper) # ['HELLO', 'WORLD', 'PYTHON']
# Get the length of each word
lengths = [len(w) for w in words]
print(lengths) # [5, 5, 6]
# Strip and title-case a list of names from user input
raw = [" alice ", " BOB", "CAROL "]
clean = [name.strip().title() for name in raw]
print(clean) # ['Alice', 'Bob', 'Carol']
transform.py
Filtering with if
Add a filter condition after the for clause to
include only items that match:
# [ expression for item in iterable if condition ]
numbers = range(20)
evens = [n for n in numbers if n % 2 == 0]
print(evens) # [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
big_evens = [n for n in numbers if n % 2 == 0 if n > 10]
print(big_evens) # [12, 14, 16, 18] — two conditions (AND)
# Filter a list of strings — keep only non-empty, stripped items
entries = ["Alice", "", " ", "Bob", "Carol", ""]
valid = [e.strip() for e in entries if e.strip()]
print(valid) # ['Alice', 'Bob', 'Carol']
filtering.py
if conditions after the for are combined with AND.
[x for x in data if cond1 if cond2] is equivalent to
[x for x in data if cond1 and cond2].
Inline if/else (Ternary Expression)
To transform based on a condition (rather than filter), put the
if/else in the expression part (before the for):
# [ value_if_true if condition else value_if_false for item in iterable ]
numbers = range(10)
# Label each number as "even" or "odd"
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
print(labels)
# ['even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd', 'even', 'odd']
# Clamp values to a maximum of 5
clamped = [n if n <= 5 else 5 for n in range(10)]
print(clamped) # [0, 1, 2, 3, 4, 5, 5, 5, 5, 5]
ternary.py
ifafter thefor→ filter (some items excluded)if/elsebefore thefor→ transform (all items included, some changed)
Nested Loops in Comprehensions
You can use multiple for clauses to iterate over nested iterables.
The order mirrors nested for loops — outermost first:
# Flatten a 2-D list
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 loop:
# for row in matrix:
# for n in row:
# flat.append(n)
# Cartesian product of two lists
colours = ["red", "green"]
sizes = ["S", "M", "L"]
combos = [(c, s) for c in colours for s in sizes]
print(combos)
# [('red', 'S'), ('red', 'M'), ('red', 'L'),
# ('green', 'S'), ('green', 'M'), ('green', 'L')]
nested_loops.py
for clauses in a single comprehension quickly
becomes unreadable. If you need more than two levels, use plain loops or
itertools.product().
Dict Comprehensions
Build a dictionary in one expression using {key: value for …}:
words = ["apple", "banana", "cherry"]
# Map each word to its length
word_lengths = {w: len(w) for w in words}
print(word_lengths)
# {'apple': 5, 'banana': 6, 'cherry': 6}
# Invert a dictionary (swap keys and values)
original = {"a": 1, "b": 2, "c": 3}
inverted = {v: k for k, v in original.items()}
print(inverted) # {1: 'a', 2: 'b', 3: 'c'}
# Filter: keep only items with a value > 1
filtered = {k: v for k, v in original.items() if v > 1}
print(filtered) # {'b': 2, 'c': 3}
# Normalise scores to 0–1 range
raw_scores = {"Alice": 88, "Bob": 95, "Carol": 73}
max_score = max(raw_scores.values())
normalised = {name: score / max_score for name, score in raw_scores.items()}
print(normalised)
# {'Alice': 0.926..., 'Bob': 1.0, 'Carol': 0.768...}
dict_comprehensions.py
Set Comprehensions
Like list comprehensions but with curly braces {…} — the result is a
set (unique, unordered):
numbers = [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
unique_squares = {n ** 2 for n in numbers}
print(unique_squares) # {1, 4, 9, 16} — duplicates removed
words = ["Hello", "hello", "WORLD", "world", "Python"]
unique_lower = {w.lower() for w in words}
print(unique_lower) # {'hello', 'world', 'python'}
set_comprehensions.py
{} with no : gives a set comprehension.
{} alone (no comprehension, no items) is an empty dict,
not an empty set — use set() for that.
Generator Expressions
Generator expressions look like list comprehensions but use parentheses instead of square brackets. They produce values lazily — one at a time — without building the full list in memory:
# List comprehension — builds the whole list at once
squares_list = [n ** 2 for n in range(1_000_000)] # ~8 MB in memory
# Generator expression — computes values on demand
squares_gen = (n ** 2 for n in range(1_000_000)) # nearly zero memory
# Both are iterable
print(sum(squares_list)) # 333332833333500000
print(sum(squares_gen)) # 333332833333500000 (same result, much less RAM)
# Generators can only be iterated ONCE
gen = (n * 2 for n in range(5))
print(list(gen)) # [0, 2, 4, 6, 8]
print(list(gen)) # [] — exhausted!
generator_expr.py
Passing a generator directly to a function
data = [3, -1, 4, -1, 5, -9, 2, 6]
# No need to build a list — the generator feeds sum() directly
total_positive = sum(x for x in data if x > 0)
print(total_positive) # 20
# Works with any function that accepts an iterable
longest = max(len(word) for word in ["hi", "hello", "hey"])
print(longest) # 5
has_negative = any(x < 0 for x in data)
print(has_negative) # True — stops early on first match!
gen_to_func.py
sum(x for x in data) instead of sum((x for x in data)).
Nested Comprehensions
The expression part of a comprehension can itself be a comprehension — useful for building 2-D structures like matrices:
# 3×3 identity matrix
identity = [[1 if i == j else 0 for j in range(3)] for i in range(3)]
for row in identity:
print(row)
# [1, 0, 0]
# [0, 1, 0]
# [0, 0, 1]
# Transpose a matrix (rows become columns)
matrix = [[1, 2, 3],
[4, 5, 6],
[7, 8, 9]]
transposed = [[row[i] for row in matrix] for i in range(3)]
for row in transposed:
print(row)
# [1, 4, 7]
# [2, 5, 8]
# [3, 6, 9]
nested_comprehension.py
When NOT to Use Comprehensions
Comprehensions are powerful but can hurt readability when overused:
# ❌ Too complex — impossible to read at a glance
result = [f(x) for x in [g(y) for y in data if pred(y)] if check(x)]
# ✓ Break it into named steps
filtered_y = [g(y) for y in data if pred(y)]
result = [f(x) for x in filtered_y if check(x)]
# ❌ Side effects in a comprehension — use a loop instead
[print(x) for x in data] # works, but misleading (returns a list of None)
# ✓ Use a loop for side effects
for x in data:
print(x)
# ❌ Comprehension just for iteration (no collection needed)
results = [process(x) for x in data] # if you never use 'results'
# ✓ If you only need iteration, use a for loop or generator expression
for x in data:
process(x)
when_not_to.py
| Situation | Use |
|---|---|
| Transform / filter a collection → new list | List comprehension ✓ |
| Build a dict or set from an iterable | Dict / set comprehension ✓ |
| Feed a large iterable to a function once | Generator expression ✓ |
| Multiple side effects per iteration | Plain for loop ✓ |
| Complex multi-step logic per item | Plain for loop ✓ |
| Three or more nested loops | Plain loops or itertools ✓ |
Performance
import timeit
# List comprehension vs map() vs for loop
data = range(10_000)
t1 = timeit.timeit(lambda: [x * 2 for x in data], number=1000)
t2 = timeit.timeit(lambda: list(map(lambda x: x * 2, data)), number=1000)
t3 = timeit.timeit(lambda: [x * 2 for x in data], number=1000)
# Comprehensions are typically fastest for simple transformations.
# map() with a named function (not lambda) can match or beat them.
# For generators, memory wins over speed when the whole list isn't needed.
performance.py
Primary sources: Python Docs — List Comprehensions · Python Docs — Displays for Lists, Sets, Dicts · PEP 289 — Generator Expressions
Ask your AI tutor! Not sure whether to use a list comprehension or a generator? Struggling to read a nested comprehension you found in the wild? Paste it and ask for a plain-loop equivalent — it's a great way to build intuition.
💻 Exercises
Given the list of products below, use comprehensions to produce:
products = [
{"name": "Widget", "price": 9.99, "in_stock": True},
{"name": "Gadget", "price": 24.99, "in_stock": False},
{"name": "Doohickey","price": 4.99, "in_stock": True},
{"name": "Thingamajig","price": 49.99,"in_stock": True},
{"name": "Whatsit", "price": 14.99, "in_stock": False},
]
- A list of names of all in-stock products.
- A dict mapping each product name to its price (all products).
- A list of names of in-stock products with a price under £15, in upper-case.
Show solution
products = [
{"name": "Widget", "price": 9.99, "in_stock": True},
{"name": "Gadget", "price": 24.99, "in_stock": False},
{"name": "Doohickey", "price": 4.99, "in_stock": True},
{"name": "Thingamajig", "price": 49.99, "in_stock": True},
{"name": "Whatsit", "price": 14.99, "in_stock": False},
]
# 1. Names of in-stock products
in_stock_names = [p["name"] for p in products if p["in_stock"]]
print(in_stock_names)
# ['Widget', 'Doohickey', 'Thingamajig']
# 2. Dict: name → price
price_map = {p["name"]: p["price"] for p in products}
print(price_map)
# {'Widget': 9.99, 'Gadget': 24.99, ...}
# 3. In-stock, under £15, upper-case
cheap_in_stock = [
p["name"].upper()
for p in products
if p["in_stock"] and p["price"] < 15
]
print(cheap_in_stock)
# ['WIDGET', 'DOOHICKEY']
Write a function transpose(matrix) that takes a 2-D list (list of
equal-length rows) and returns its transpose — using a nested list comprehension.
Do not use zip().
m = [[1, 2, 3],
[4, 5, 6]]
print(transpose(m))
# [[1, 4], [2, 5], [3, 6]]
Show solution
def transpose(matrix):
rows = len(matrix)
cols = len(matrix[0])
return [[matrix[r][c] for r in range(rows)] for c in range(cols)]
m = [[1, 2, 3], [4, 5, 6]]
print(transpose(m)) # [[1, 4], [2, 5], [3, 6]]
# Verify with a square matrix
square = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
for row in transpose(square):
print(row)
# [1, 4, 7]
# [2, 5, 8]
# [3, 6, 9]
Given the nested data below, use comprehensions (no explicit loops) to:
students = [
{"name": "Alice", "grades": [88, 92, 79, 95]},
{"name": "Bob", "grades": [70, 65, 80, 72]},
{"name": "Carol", "grades": [95, 98, 100, 92]},
{"name": "Dave", "grades": [55, 60, 58, 62]},
]
- A dict mapping each student's name to their average grade (rounded to 1 dp).
- A list of names of students whose average is ≥ 80.
- A flat list of ALL grades across all students.
Show solution
students = [
{"name": "Alice", "grades": [88, 92, 79, 95]},
{"name": "Bob", "grades": [70, 65, 80, 72]},
{"name": "Carol", "grades": [95, 98, 100, 92]},
{"name": "Dave", "grades": [55, 60, 58, 62]},
]
# 1. Name → average grade
averages = {
s["name"]: round(sum(s["grades"]) / len(s["grades"]), 1)
for s in students
}
print(averages)
# {'Alice': 88.5, 'Bob': 71.75, 'Carol': 96.25, 'Dave': 58.75}
# 2. Students with average >= 80
high_achievers = [
s["name"]
for s in students
if sum(s["grades"]) / len(s["grades"]) >= 80
]
print(high_achievers) # ['Alice', 'Carol']
# 3. Flat list of all grades
all_grades = [grade for s in students for grade in s["grades"]]
print(all_grades)
# [88, 92, 79, 95, 70, 65, 80, 72, 95, 98, 100, 92, 55, 60, 58, 62]