List comprehensions
Building a new list from an old one is so common that Python has a dedicated syntax for it. Once it clicks, three lines become one and the one reads better.
prices = [250, 120, 80]
with_tax = []
for price in prices:
with_tax.append(round(price * 1.15))
print(with_tax)
print([round(price * 1.15) for price in prices])
[288, 138, 92] [288, 138, 92]
Same answer. The comprehension says the whole intention on one line: the taxed price, for every price.
The shape
[ expression for item in iterable ]
Read it out loud in that order — "round(price * 1.15), for price in prices" —
and it is an English sentence. The for part is identical to a normal loop
header; the expression at the front is what goes into the new list.
names = ["ayesha", "bilal", "sana"]
print([name.title() for name in names])
print([len(name) for name in names])
print([n * n for n in range(1, 6)])
['Ayesha', 'Bilal', 'Sana'] [6, 5, 4] [1, 4, 9, 16, 25]
It always produces a new list. The original is untouched, which is usually what you want and is worth knowing when you expected otherwise.
Adding a condition
Put an if on the end and only the items passing it are included.
marks = [72, 45, 88, 30, 91]
print([mark for mark in marks if mark >= 50])
print([mark for mark in marks if mark % 2 == 0])
[72, 88, 91] [72, 88, 30]
The expression at the front can still transform:
marks = [72, 45, 88, 30, 91]
print([f"{mark}%" for mark in marks if mark >= 50])
['72%', '88%', '91%']
Filter and transform in one line, and the order reads correctly: what to keep is at the end, what to produce is at the front.
The other if, and why it goes somewhere else
There is a second place an if can appear, and mixing the two up is the
commonest comprehension bug.
marks = [72, 45, 88]
print([mark for mark in marks if mark >= 50])
print(["pass" if mark >= 50 else "fail" for mark in marks])
[72, 88] ['pass', 'fail', 'pass']
ifat the end is a filter. It decides whether an item is included, so the result can be shorter than the input.if ... elseat the front is a ternary — part of the expression. It decides what value to produce, so the result is always the same length.
The giveaway is else. A filter cannot have one, because there is nothing to
put in the list when the test fails. If you have written else, it belongs at
the front.
Nested loops
Two for clauses read in the same order as the loops they replace: outer
first, inner second.
grid = [[1, 2], [3, 4], [5, 6]]
print([value for row in grid for value in row])
[1, 2, 3, 4, 5, 6]
That order feels backwards to most people, because the expression uses
value, which comes from the second clause. The rule that fixes it: write
the nested loop first, then lift the lines out in order.
for row in grid: # first for
for value in row: # second for
flat.append(value) # the expression
Flattening is the one nested comprehension worth using. Beyond that, the readability drops off a cliff, and a real loop is better.
Dictionary and set comprehensions
The same syntax with different brackets builds the other collections.
names = ["Ayesha", "Bilal"]
print({name: len(name) for name in names})
print(sorted({len(name) for name in names}))
{'Ayesha': 6, 'Bilal': 5}
[5, 6]A dict comprehension needs key: value in front. A set comprehension
looks like a list comprehension in curly brackets, and deduplicates for free.
Note the sorted() around the set — a set has no order, so printing one
directly is not reproducible, as
the sets lesson explains.
Inverting a dictionary is the classic one-liner:
stock = {"chai": 12, "coffee": 4}
print({count: item for item, count in stock.items()})
print({item: count for item, count in stock.items() if count > 5})
{12: 'chai', 4: 'coffee'}
{'chai': 12}Curly brackets do not give you a tuple
squares = (n * n for n in range(5))
print(type(squares))
print(list(squares))
<class 'generator'> [0, 1, 4, 9, 16]
Round brackets produce a generator, not a tuple. A generator computes each value as it is asked for rather than building the whole list in memory, which matters when the sequence is huge or infinite.
The catch is that it is consumed once:
squares = (n * n for n in range(5))
print(list(squares))
print(list(squares))
[0, 1, 4, 9, 16] []
The second call gets nothing, because the first one walked to the end. If you need the values twice, use a list.
Where generators shine is feeding a function that only needs one pass:
marks = [72, 45, 88]
print(sum(mark for mark in marks if mark >= 50))
print(max(len(name) for name in ["Ayesha", "Bilal"]))
160 6
No brackets needed when the generator is the only argument, and nothing larger than one value is ever held in memory.
When not to use one
A comprehension is for building a collection. It is not a shorter way to write a loop.
names = ["Ayesha", "Bilal"]
# wrong: this builds a list of Nones and throws it away
[print(name) for name in names]
Ayesha Bilal
That works, and it is the wrong tool: it constructs a two-item list of
Nones purely as a side effect of printing. Use a plain for loop when you
want to do something rather than make something.
The other stopping point is length. When a comprehension needs a filter, a ternary and a nested loop, it has stopped being one readable line:
grid = [[1, 2], [3, 4], [5, 6]]
hard = [v * 2 if v % 2 == 0 else v for row in grid for v in row if v != 3]
print(hard)
[1, 4, 8, 5, 12]
Correct, and nobody wants to be the person who has to change it. Two levels of logic is the point at which a loop with a name and a comment is the kinder choice.
What to remember
[expr for x in items]— transform every item.[expr for x in items if test]— filter, then transform.[a if test else b for x in items]— the ternary goes at the front.{k: v for ...}and{expr for ...}— dicts and sets.(expr for ...)— a generator, consumed once.
And when it stops fitting comfortably on one line, that is the language telling you to use a loop.
Next: writing your own functions.
Prefer learning with a teacher?
WasiLearn Academy runs small-group classes covering this material, online and in Karachi.
See the classes