🔵 Intermediate

Comprehensions

📖 Lesson 16 ⏱ 30 min 🧪 5 questions 💻 3 exercises

🎯 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 for loop 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
Comprehensions are not just shorter — they are often faster than equivalent loops because Python optimises them internally. They are also an important part of idiomatic Python that you will see everywhere in real codebases.

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
Multiple 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
Placement rule:
  • if after the forfilter (some items excluded)
  • if/else before the fortransform (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
Three or more nested 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
When passing a generator expression as the only argument to a function, you can drop the extra parentheses: 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
SituationUse
Transform / filter a collection → new listList comprehension ✓
Build a dict or set from an iterableDict / set comprehension ✓
Feed a large iterable to a function onceGenerator expression ✓
Multiple side effects per iterationPlain for loop ✓
Complex multi-step logic per itemPlain for loop ✓
Three or more nested loopsPlain 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
🤖

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

01 Filtering & Transforming

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},
]
  1. A list of names of all in-stock products.
  2. A dict mapping each product name to its price (all products).
  3. 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']
02 Matrix Transpose

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]
03 Nested Data Extraction

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]},
]
  1. A dict mapping each student's name to their average grade (rounded to 1 dp).
  2. A list of names of students whose average is ≥ 80.
  3. 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]