Level 2 · The Toolbox

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:

  1. Take each n from numbers,
  2. keep it only if the condition is true,
  3. and put n * 2 in 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']
🤔 Two different ifs, two different places

[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]
RHETORIC[Medium: Success]

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
Exercise 1

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}
Exercise 2

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'.

Exercise 3

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.

+100 XP