Comprehensions ✨
Take a loop that builds a list, and fold it into a single line that reads like a sentence. Used well this is Python at its best. Used badly it is a war crime.
From loop to comprehension
Here is the loop you already know:
squares = []
for n in range(1, 6):
squares.append(n * n)
print(squares)
[1, 4, 9, 16, 25]
And here it is again:
squares = [n * n for n in range(1, 6)]
print(squares)
[1, 4, 9, 16, 25]
The pieces are the same, rearranged. The thing you append comes first, then the loop. Read it aloud: "n times n, for each n in one to five".
[ n * n for n in range(1, 6) ]
^^^^^ ^^^^^^^^^^^^^^^^^^^^
what where it comes from
you keep
Adding a filter
numbers = [4, 9, 15, 22, 30, 7]
evens = [n for n in numbers if n % 2 == 0]
print(evens)
big_doubled = [n * 2 for n in numbers if n > 10]
print(big_doubled)
[4, 22, 30]
[30, 44, 60]
The if goes at the end and decides what gets in. Reading order:
- Take each
nfromnumbers, - keep it only if the condition is true,
- and put
n * 2in the new list.
Transforming text
crew = [" guybrush ", "ELAINE", " otis "]
tidy = [name.strip().title() for name in crew]
print(tidy)
lengths = [len(name) for name in tidy]
print(lengths)
initials = [name[0] for name in tidy]
print(initials)
['Guybrush', 'Elaine', 'Otis']
[8, 6, 4]
['G', 'E', 'O']
Dictionary and set comprehensions
words = ["grog", "sword", "map"]
lengths = {word: len(word) for word in words}
print(lengths)
first_letters = {word[0] for word in words}
print(sorted(first_letters))
scores = {"Guybrush": 95, "Elaine": 88, "Otis": 42}
passed = {name: score for name, score in scores.items() if score >= 50}
print(passed)
{'grog': 4, 'sword': 5, 'map': 3}
['g', 'm', 's']
{'Guybrush': 95, 'Elaine': 88}
Same syntax, different brackets. [ ] makes a list, { } with a
colon makes a dictionary, { } without makes a set. Filtering a dictionary
down to the entries you want is one of the most useful lines in day-to-day Python.
The conditional version
numbers = [4, 9, 15, 22]
labels = ["even" if n % 2 == 0 else "odd" for n in numbers]
print(labels)
['even', 'odd', 'odd', 'even']
[x for x in items if cond] filters: the if is at the end and decides what is included. [a if cond else b for x in items] chooses: the if is at the front and decides what value each item becomes. If you need an else, it goes at the front. This trips up everyone.
Nested loops in a comprehension
pairs = [(a, b) for a in "AB" for b in [1, 2]]
print(pairs)
grid = [[1, 2], [3, 4], [5, 6]]
flat = [n for row in grid for n in row]
print(flat)
[('A', 1), ('A', 2), ('B', 1), ('B', 2)]
[1, 2, 3, 4, 5, 6]
The loops read in the same order you would write them normally: outer first, inner second. Flattening a list of lists is the common case and worth memorising as a phrase.
Generator expressions: the lazy cousin
numbers = range(1, 1_000_001)
# builds a million-item list in memory first
total_list = sum([n * n for n in numbers])
# produces one value at a time and never stores them all
total_gen = sum(n * n for n in numbers)
print(total_list == total_gen)
print(f"{total_gen:,}")
True
333,333,833,333,500,000
Round brackets (or none at all, inside a function call) give you a
generator expression: it computes values on demand instead of building
the whole list. For a million squares that is the difference between using about 40MB of
memory and using almost none. When you are feeding straight into
sum, max, any or a for loop, drop the
square brackets. Lesson 34 goes deeper.
any and all
scores = [95, 88, 42, 71]
print(any(s < 50 for s in scores))
print(all(s >= 40 for s in scores))
print(sum(1 for s in scores if s >= 70))
True
True
3
any and all plus a generator expression is how you ask "is
there at least one" and "is every single one" in a single readable line. They also stop
early as soon as the answer is decided.
When not to use one
# Do not do this to people.
result = [y for x in range(10) if x % 2 == 0 for y in range(x) if y % 3 == 0 and y > 1]
print(result)
[3, 3, 3, 6]
You wrote it, and today you understand it. That is not the test. The test is whether a colleague can read it at speed on a Friday afternoon, or whether you can in March.
If a comprehension needs more than one loop and one condition, or it does not fit comfortably on one line, write the loop. Nobody has ever been fired for clarity.
Also: if the loop is doing something rather than collecting something, use a loop.
# wrong: building a list of Nones purely for the side effect
# [print(n) for n in range(3)]
# right
for n in range(3):
print(n)
0
1
2
Rewrite as comprehensions
Turn each of these into one line.
# A
result = []
for word in ["grog", "map", "sword"]:
result.append(word.upper())
# B
long_words = []
for word in ["grog", "a", "sword", "of"]:
if len(word) > 2:
long_words.append(word)
# C
lengths = {}
for word in ["grog", "map"]:
lengths[word] = len(word)Reveal solution
result = [word.upper() for word in ["grog", "map", "sword"]]
long_words = [w for w in ["grog", "a", "sword", "of"] if len(w) > 2]
lengths = {word: len(word) for word in ["grog", "map"]}
print(result)
print(long_words)
print(lengths)
['GROG', 'MAP', 'SWORD']
['grog', 'sword']
{'grog': 4, 'map': 3}FizzBuzz in one line
Produce a list of the FizzBuzz results for 1 to 15 using a single comprehension. Then decide whether you would actually ship it.
Reveal solution
result = [
"FizzBuzz" if n % 15 == 0
else "Fizz" if n % 3 == 0
else "Buzz" if n % 5 == 0
else str(n)
for n in range(1, 16)
]
print(result)
['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz', '11', 'Fizz', '13', '14', 'FizzBuzz']It works, and chained conditional expressions are legal. Would you ship it? Probably not: the loop version from Lesson 9 is clearer and just as short in practice. The right answer to 'can Python do this in one line' is often 'yes, and no'.
Filter records
From a list of dictionaries, produce a list of the names of everyone paid more than 50, sorted alphabetically.
crew = [
{"name": "Guybrush", "pay": 100},
{"name": "Otis", "pay": 40},
{"name": "Elaine", "pay": 250},
{"name": "Meathook", "pay": 45},
]Reveal solution
crew = [
{"name": "Guybrush", "pay": 100},
{"name": "Otis", "pay": 40},
{"name": "Elaine", "pay": 250},
{"name": "Meathook", "pay": 45},
]
well_paid = sorted(m["name"] for m in crew if m["pay"] > 50)
print(well_paid)
['Elaine', 'Guybrush']A generator expression passed straight to sorted, with no intermediate list. This is what fluent Python looks like: not clever, just direct.