The pattern is so common it deserves a name. You have a sequence, you want a new one, and every item needs the same small change — each price rounded, each name turned into its length, each number squared. Written as a loop, that is three lines every time: make an empty list, loop, append. A comprehension folds those three lines into one expression that reads almost like a sentence.
Read a comprehension left to right: first what each item becomes, then which sequence it draws from.
nums = [1, 2, 3, 4]
squares = [n * n for n in nums] # [1, 4, 9, 16]
flowchart LR I["[1, 2, 3, 4]"] --> C["n * n for n in nums"] C --> O["[1, 4, 9, 16]"] style C fill:#3776ab,color:#fff
The shape, and how to read it
Square brackets build a list; curly braces build a set or a dict. Inside, three parts sit in a fixed order: an output expression on the left (what each item turns into), a for clause in the middle (where the items come from), and an optional if clause at the end (which items to keep).
[n * n for n in nums] turns each n into its square, while the loop pulls every n out of nums. The output expression runs once per surviving item, and the results are collected in order.
nums = [1, 2, 3, 4, 5, 6]
squares = [n * n for n in nums]
print(squares)
evens = [n for n in nums if n % 2 == 0]
print(evens)
lengths = {w: len(w) for w in ['cat', 'hi', 'ok']}
print(lengths)
Transform and filter together
Run that example and three things happen. squares transforms every number. evens filters — the output expression is just n, so each item is kept unchanged and only the test decides. lengths builds a dict, mapping each word to a count of its letters.
That middle case is worth pausing on. When you only want to keep some items without changing them, the output expression and the loop variable share the same name. [n for n in nums if n % 2 == 0] looks odd at first because n appears twice, but it means keep n, exactly as it is, whenever n is even.
Filtering: the if clause goes last
Add an if at the end to keep only the items that pass a test. The position matters and it surprises people: the filter sits after the for, not glued to the output expression.
evens = [n for n in nums if n % 2 == 0] # [2, 4]
Read it as collect n, for each n in nums, but only when n is even.
Do not confuse this filter with a ternary expression. [n if n > 0 else 0 for n in vals] puts the if before the for, because there it chooses the output value for every item rather than dropping any of them.
flowchart LR
subgraph F["filter: if AFTER for"]
A1["keeps only items that pass"] --> A2["drops the rest"]
end
subgraph T["ternary: if BEFORE for"]
B1["picks the output value"] --> B2["runs for every item"]
end
style A1 fill:#3776ab,color:#fff
style B1 fill:#b45309,color:#fff
lengths = {w: len(w) for w in ['cat', 'hi']}
print(lengths)
unique = {n % 3 for n in [1, 4, 7, 10]}
print(unique)
squares_of_evens = [n * n for n in range(6) if n % 2 == 0]
print(squares_of_evens)
Dictionaries and sets, the same way
Swap the square brackets for curly braces and you can build other collections in one line. A dict comprehension writes key: value as the output expression; a set comprehension drops the colon, and the duplicates with it.
The dict above maps each word to its length. The set collects remainders, and because sets cannot hold repeats, four values that all leave remainder 1 collapse to a single entry.
When dict keys collide
A dict cannot hold the same key twice, so a dict comprehension quietly keeps the last value when keys repeat. Mapping the words ['hi', 'ho', 'hi'] to their lengths writes 'hi' once, overwrites it the second time, and the result holds two entries rather than three.
That is occasionally exactly what you want — deduplicating by a key. More often it is a silent bug, because nothing warns you that two source items fought for the same slot. If your keys might repeat and every value matters, a dict is the wrong shape; reach for a list of pairs instead.
flowchart LR C["a comprehension"] --> E["unrolls into"] E --> S1["make an empty list"] E --> S2["loop over the source"] E --> S3["append each result"] style C fill:#3776ab,color:#fff
The loop it replaces
A comprehension is a plain loop, squashed. Unrolling [n * n for n in nums if n > 0] by hand gives the four lines it stands in for: create an empty list, loop over the source, test the condition, and append the result. Every part of that loop maps onto a clause in the brackets, in the same order.
Keeping that translation in mind is how you read other people's comprehensions, and how you decide whether to write one. If the unrolled loop would be clear, the comprehension probably is too; if it would need comments to explain, prefer the loop.
Write evens_between(lo, hi) that returns a list of the even numbers from lo to hi inclusive, using a comprehension.
def evens_between(lo, hi):
# one comprehension over range(lo, hi + 1)
pass
Loop over
range(lo, hi + 1)to include both ends.Keep only even values with
if n % 2 == 0.
def evens_between(lo, hi):
return [n for n in range(lo, hi + 1) if n % 2 == 0]
What does this print? Each number is squared into a new list.
nums = [1, 2, 3, 4] squares = [n * n for n in nums] print(squares)
1 squared, 2 squared, 3 squared, 4 squared.
Python prints a list with brackets and comma-separated items.
Write word_lengths(words) that returns a dict mapping each word to its length, using a dict comprehension. So word_lengths(['cat', 'ok']) gives {'cat': 3, 'ok': 2}.
def word_lengths(words):
# one dict comprehension
pass
Curly braces with a colon: {w: len(w) for w in words}.
The key is the word; the value is its length.
def word_lengths(words):
return {w: len(w) for w in words}
This comprehension is meant to return the squares of only the even numbers in nums, but it squares every number — the filter is missing. Add the if clause in the right place (after the for).
def square_evens(nums):
return [n * n for n in nums] # bug: squares every number
The if goes after 'for n in nums'.
Keep only even values: if n % 2 == 0.
def square_evens(nums):
return [n * n for n in nums if n % 2 == 0]
Write long_word_lengths(words, min_len) that returns a dict mapping each word longer than min_len to its length. This combines a dict comprehension with a filter — copy carefully, because both the colon and the if matter.
def long_word_lengths(words, min_len):
# {w: len(w) for ... if ...}
pass
Start from {w: len(w) for w in words} and add 'if len(w) > min_len'.
The filter sits after the for, exactly as it would in a list comprehension.
def long_word_lengths(words, min_len):
return {w: len(w) for w in words if len(w) > min_len}
Recap
- A comprehension is one expression —
[output for item in source]— read left to right. - Filter with an
ifplaced after thefor; use a ternary (x if cond else y) before theforto pick each output value instead. - Curly braces make a dict (
{k: v for ...}) or a set ({v for ...}); sets throw out duplicates for free. - Reach for a comprehension when it reads cleanly, and for a loop when you need side effects or the logic grows long.
Next you will pass functions around as values, where comprehensions often do the heavy lifting in a single readable line.