PYTHON / LISTS AND TUPLES
List comprehensions
Build new lists in one expression with map-and-filter comprehensions, and know exactly when a plain loop is the better choice.
What you will learn
- Rewrite an append-in-a-loop pattern as an equivalent list comprehension
- Filter items with a trailing if, and transform them with a conditional expression
- Chain two for clauses in the order the equivalent nested loops would run
- Recognise that the comprehension's loop variable does not leak into the enclosing scope
Understanding List comprehensions
A list comprehension is a single expression that produces a brand new list. The mental model is three slots: what to collect, what to iterate over, and optionally which items to keep. `[len(w) for w in words]` is exactly the same work as creating an empty list, looping over `words`, and appending `len(w)` each time; the comprehension just removes the bookkeeping so the intent is the only thing left on the page.
Reading order and execution order differ, and that is the main source of confusion. The `for` clause runs first, then the optional `if` is tested for that one item, and only if the test passes is the leading expression evaluated and its value appended. That ordering is practical, not academic: `[1 / n for n in nums if n != 0]` never divides by zero, because the filter gates the expression per item rather than filtering the finished list. When you write two `for` clauses, they nest left to right, so the leftmost one is the outer loop and the rightmost advances fastest.
Two further properties follow from a comprehension being an expression. It has its own scope, so its loop variable cannot overwrite a same-named name outside it, unlike a normal `for` statement. And because it always builds a list, using one purely for side effects, such as `[print(x) for x in items]`, constructs and discards a list of `None` values. Comprehensions also avoid re-looking-up `.append` on every iteration, which is where their modest speed edge comes from, but that advantage disappears the moment the logic needs several statements, so reach for a loop then.
words = ["alpha", "beta", "gamma", "delta"]
lengths = [len(w) for w in words]
print(lengths)
selected = [w.upper() for w in words if len(w) == 5]
print(selected)
manual = []
for w in words:
manual.append(len(w))
print(manual == lengths)A list comprehension is one expression that iterates, optionally filters, and only then evaluates the collected value for each surviving item.
Worked examples
Filtering versus transforming
Shows that a trailing if shortens the result while a conditional expression keeps every item.
nums = [-3, 4, -1, 0, 7]
clamped = [n if n > 0 else 0 for n in nums]
print(clamped)
positives = [n for n in nums if n > 0]
print(positives)
print(len(clamped), len(positives))Example explained
Line 1`n if n > 0 else 0` is a conditional expression in the collect slot, so it yields a value for every item.
Line 2`if n > 0` placed after the `for` is a filter, so rejected items produce nothing at all.
Line 3The lengths differ for that reason: 5 items in, 5 out for the first form, 2 out for the second.
Line 4Writing `[n if n > 0 for n in nums]` is a SyntaxError, because a conditional expression must have an `else`.
Two for clauses nest left to right
Demonstrates that the rightmost for clause is the inner loop.
sizes = ["S", "M"]
colors = ["red", "blue"]
skus = [size + "-" + color for size in sizes for color in colors]
print(skus)
print(len(skus))Example explained
Line 1`for size in sizes` comes first, so it behaves as the outer loop and changes most slowly.
Line 2`for color in colors` is the inner loop, so both colors are emitted before `size` advances to "M".
Line 3The result length is 2 * 2, since nothing is filtered out.
Line 4Swapping the two clauses would still give four items, but grouped by color instead.
The loop variable stays inside
Shows that a comprehension has its own scope and cannot clobber an outer name.
n = 99
squares = [n * n for n in range(4)]
print(squares)
print(n)Example explained
Line 1Inside the comprehension, `n` refers to the comprehension's own local `n`, taking values 0 to 3.
Line 2After the comprehension finishes, the outer `n` is still 99 because the comprehension never assigned to it.
Line 3An ordinary `for n in range(4):` statement would have left `n` equal to 3, which is the difference to remember.
Important notes
A comprehension always builds the whole list in memory; if you only iterate over the result once, `sum(len(w) for w in words)` uses a generator expression and allocates nothing.
Filtering happens before the leading expression runs, which is what makes guards like `if n != 0` safe against errors in that expression.
Common mistakes
Putting the condition before the `for` without an `else`, as in `[x if x > 0 for x in nums]`, which fails immediately with a SyntaxError rather than filtering.
Using a comprehension for its side effects, such as `[print(x) for x in items]`, which prints correctly but also allocates a throwaway list of `None` values.
Ordering nested `for` clauses backwards when the inner iterable depends on the outer variable, for example `[c for c in word for word in words]`, which raises NameError because `word` is not bound yet.
Try it yourself
Change, predict, then run
Start from `readings = ["12.5", "3.0", "18.2", "7.7"]` and write one comprehension that converts each string to a float and keeps only values above 10. Print the resulting list and its length.
Open the Python workspaceCheck your understanding
For `[f(x) for x in xs if p(x)]`, which description matches what Python actually does?
- For each item, p is tested first, and f is evaluated only for items that pass
- f is evaluated for every item, and p then filters the resulting values
- The result always has the same length as xs, since f runs once per item
- p and f are both evaluated for every item, but failing items append None
Show answer
Per item the order is iterate, test, then collect, so f never sees an item the filter rejected; that is why `[1 / n for n in nums if n != 0]` is safe. Option 1 is tempting because f is written leftmost, but reading order is not execution order, and if f ran first the zero-division guard would be useless.