List Comprehensions
A list comprehension builds a new list from an existing iterable in a single, readable expression: "the square of each number", "every name that starts with A". They're one of Python's most loved features and replace many short for loops.
From loop to comprehension
numbers = [1, 2, 3, 4, 5] squares = [] for n in numbers: squares.append(n * n) print(squares) squares = [n * n for n in numbers] print(squares)
[1, 4, 9, 16, 25] [1, 4, 9, 16, 25]
Read [n * n for n in numbers] as "n * n for each n in numbers". The expression comes first, then the loop.
Filtering with if
words = ["apple", "Banana", "avocado", "cherry", "Apricot"] a_words = [w for w in words if w.lower().startswith("a")] print(a_words) long_upper = [w.upper() for w in words if len(w) > 6] print(long_upper)
['apple', 'avocado', 'Apricot'] ['AVOCADO', 'APRICOT']
An if at the end keeps only the items that pass the test.
Choosing a value with if/else
To transform every item but pick between two results, put a conditional expression at the front:
scores = [91, 45, 78, 30] results = ["pass" if s >= 50 else "fail" for s in scores] print(results) clipped = [min(max(s, 40), 90) for s in scores] print(clipped)
['pass', 'fail', 'pass', 'fail'] [90, 45, 78, 40]
Remember the difference: if at the end filters (fewer items); … if … else … at the front chooses a value (same number of items).
Nested loops
colors = ["red", "blue"] sizes = ["S", "M"] variants = [f"{c}-{s}" for c in colors for s in sizes] print(variants) matrix = [[1, 2, 3], [4, 5, 6]] flat = [n for row in matrix for n in row] print(flat) transposed = [[row[i] for row in matrix] for i in range(3)] print(transposed)
['red-S', 'red-M', 'blue-S', 'blue-M'] [1, 2, 3, 4, 5, 6] [[1, 4], [2, 5], [3, 6]]
Multiple for clauses run in the same order as nested loops would, outer first. Beyond two levels, a regular loop is usually clearer.
Dictionary and set comprehensions
The same syntax with braces builds dictionaries and sets:
names = ["ana", "ben", "cleo", "ana"] lengths = {name: len(name) for name in names} print(lengths) unique_upper = {name.upper() for name in names} print(sorted(unique_upper)) prices = {"tea": 3.0, "cake": 4.5, "juice": 5.0} discounted = {item: round(p * 0.9, 2) for item, p in prices.items() if p > 4} print(discounted)
{'ana': 3, 'ben': 3, 'cleo': 4}
['ANA', 'BEN', 'CLEO']
{'cake': 4.05, 'juice': 4.5}Generator expressions
Using parentheses instead of brackets creates a generator, which produces items one at a time instead of building a whole list. Pass one straight into functions like sum, max, any or "".join:
numbers = range(1, 1_000_001) total = sum(n * n for n in numbers) print(total) print(any(n % 7 == 0 for n in [3, 5, 14])) print(", ".join(str(n) for n in range(5)))
333333833333500000 True 0, 1, 2, 3, 4
The sum above never stores a million squares in memory. Generators are covered properly in lesson 35.
When not to use a comprehension
- When you're doing something for its side effect (printing, saving to a file) rather than building a list — use a normal loop.
- When the logic needs several statements, error handling or deep nesting — a loop with clear steps reads better.
- When the line gets so long you have to scroll — break it up or use a loop.
Comprehensions should make code clearer. If one needs a comment to explain it, it's probably too clever.
Variables don't leak
x = "outer" squares = [x * x for x in range(3)] print(squares, x)
[0, 1, 4] outer
The loop variable inside a comprehension has its own scope, so it doesn't overwrite a variable of the same name outside.