This lesson covers the syntax the book deliberately postponed: conditional expressions, list comprehensions for map and filter, generator expressions that compute on demand, and the any and all functions.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 19 — The Goodies
§19.1-19.4, pp. 183-185
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 183-185 — the pages these objectives are drawn from
Warm-up
The book has been withholding syntax on purpose.
Discussion prompt
Everything in this chapter could have appeared much earlier. Why might an author deliberately teach less of a language than they know?
Hint: How many ways are there to write a loop that builds a list?
Answer:
Because when there are two ways to do something, learning both at once means deciding between them before you can judge either.
The book's own statement: one of my goals for this book has been to teach you as little Python as possible. When there were two ways to do something, I picked one and avoided mentioning the other.
So everything here is optional by construction — Python provides a number of features that are not really necessary, and with them you can sometimes write code that's more concise, readable or efficient.
Concept
Python provides a number of features that are not really necessary — you can write good code without them — but with them you can sometimes write code that's more concise, readable or efficient, and sometimes all three.
That is a careful claim: sometimes, and three different benefits that do not always arrive together. A feature can make code shorter and harder to read, and this chapter is honest about which ones do.
Figure (svg): Two columns contrasting what these features are not with what they can be
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 183-183
Section
Section 1
Concept
Conditional statements are often used to choose one of two values. We can write such a statement more concisely using a conditional expression.
# the statement
if x > 0:
y = math.log(x)
else:
y = float('nan')
# the expression
y = math.log(x) if x > 0 else float('nan')| Part | Where it appears | Note |
|---|---|---|
| the value if true | comes first | math.log(x) |
| the condition | in the middle | if x > 0 |
| the value if false | last | else float('nan') |
You can almost read this line like English: y gets log-x if x is greater than 0; otherwise it gets NaN. The order is unusual — the value comes before the condition — which is what makes it read as a sentence.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 183-183
Picture it
Value, condition, alternative.
Figure (svg): A conditional expression broken into its three parts in the order they appear
And the NaN is worth a note: a special floating-point value representing Not a Number, produced here to avoid stopping the program with a ValueError.
Worked example
Not every conditional statement fits.
# converts: both branches assign to the same variable
if x > 0:
y = a
else:
y = b
# does not: the branches do different things
if x > 0:
y = a
else:
print('negative')| Shape | Convertible? | Note |
|---|---|---|
| both assign to y | a value each | convertible |
| one assigns, one prints | different actions | not convertible |
| the rule | simple expressions, returned or assigned to the same variable |
State the rule.
Why: You can replace a conditional statement with a conditional expression if both branches contain simple expressions that are either returned or assigned to the same variable.
Check the first example.
Why: Both branches assign to y, and both values are simple expressions — so it converts.
Check the second.
Why: One assigns and one prints, which are different kinds of action. An expression produces a value; it cannot do two unrelated things.
Figure (svg): The state of the program after each line of Worked example the rule for when you can convert, drawn as a ladder with one rung per traced line
A specific rule with two conditions: simple expressions, and the same destination. Statements doing anything else stay statements.
Verify: Test the rule on a branch with two statements.
Why: A branch that assigns and then prints cannot become an expression either, however simple each part is — an expression produces one value. So simple expressions is doing real work in the rule, not just describing the usual case.
Prediction
The condition is in the middle.
x = -5
y = 'positive' if x > 0 else 'not positive'
print(y)| Part | What happens | Result |
|---|---|---|
| the condition | x > 0 is False | |
| the else value | chosen | 'not positive' |
| the first value | never evaluated |
Predict first
What does this print?
Correct: not positive — the condition is False, so the value after else is chosen.
Why: The value comes before the condition, which is the unusual part of the syntax and what makes it read like a sentence. Note that only one of the two values is evaluated: if the first were math.log(x), it would not be computed for a negative x, which is exactly the point of the book's opening example.
Worked example
A recursion and an optional argument.
# a recursive factorial
def factorial(n):
return 1 if n == 0 else n * factorial(n-1)
# an optional argument
def __init__(self, name, contents=None):
self.name = name
self.pouch_contents = [] if contents == None else contents| Case | What it chooses | Note |
|---|---|---|
| the recursion | base case and recursive case | one line |
| the optional argument | a default when None was passed | one line |
| both | assign or return one of two values | the rule's shape |
Note the recursive case.
Why: Recursive functions can sometimes be rewritten using conditional expressions — the base case and the recursive case are two values.
Note the optional-argument case.
Why: Another use is handling optional arguments, where a None default is replaced by a real value inside the function.
Note why that pattern exists.
Why: The mutable-default trap from lesson 13b: the parameter defaults to None and the list is created inside, so every call gets a fresh one.
Figure (svg): Two columns comparing the statement and expression forms of the optional-argument idiom
Two idioms shortened to one line each. The second is especially common, because the None-then-create pattern appears wherever a mutable default is wanted.
Verify: Check that the optional-argument version still avoids the trap.
Why: The list is still created inside the function, once per call that omits the argument — so the shared-default bug is still avoided. Shortening the code did not change when the list is made, which is the thing that mattered.
Trap
A three-way choice is written as a conditional expression inside another one.
Extend the pattern
Why: It worked for two branches, so three should follow.
The condition is already in the middle, so a nested version puts two conditions between three values and stops reading like a sentence. The readability that justified the form is exactly what nesting removes.
Use a conditional statement for more than two branches.
A chained if / elif / else
Why: Which reads in the order the conditions are checked.
Reserve the expression for the two-value case
Why: Which is what makes it read like English.
The book's justification is legibility — you can almost read this line like English — so the test for using it is whether it still reads that way. A nested one does not.
Discrimination
Both branches must be simple expressions, returned or assigned to the same variable.
Sort into buckets
For each conditional statement, can it become a conditional expression?
Faded example
Base case first, then the condition.
Fill in the blanks
def factorial(n):
return 1 if n == 0 else n * factorial(n-1)
Why: The value comes first, then the condition, then the alternative — so the base case 1 precedes if n == 0. That word order is what makes the line read as a sentence, and it is the opposite of a conditional statement's.
Explain it to yourself
The book's opening example depends on it.
Discussion prompt
y = math.log(x) if x > 0 else float('nan') avoids a ValueError for negative x. Explain why.
Hint: Which of the two values is computed?
Answer:
Only the one the condition selects. For a negative x the condition is False, so math.log(x) is never evaluated and the error it would raise never happens.
Which is the whole point of the example: math.log would raise a ValueError, and to avoid stopping the program we generate a NaN instead.
So a conditional expression is not merely a shorter if — it has the same short-circuiting behaviour, evaluating only the branch it needs. A version that computed both would defeat the example entirely.
Section
Section 2
Concept
The bracket operators indicate that we are constructing a new list. The expression inside the brackets specifies the elements of the list, and the for clause indicates what sequence we are traversing.
# mapping: apply an operation to every element
def capitalize_all(t):
return [s.capitalize() for s in t]
# filtering: keep only some elements
def only_upper(t):
return [s for s in t if s.isupper()]| Part | What it specifies | Note |
|---|---|---|
| the brackets | a new list | being constructed |
| the expression | what each element is | s.capitalize() |
| the for clause | what is traversed | for s in t |
| an if clause | which elements to keep | filtering |
The syntax is a little awkward because the loop variable — s in this example — appears in the expression before we get to the definition.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 184-185
Picture it
The same three ingredients, rearranged.
Figure (svg): Two columns comparing an accumulator loop with the equivalent list comprehension
The awkwardness the book mentions is that the loop variable appears in the expression before the for clause defines it — you read s before you learn where it comes from.
Worked example
The same two patterns from lesson 10b.
[s.capitalize() for s in t] # map: transform each
[s for s in t if s.isupper()] # filter: keep some
[s.capitalize() for s in t if s.isupper()] # both| Form | What it does | Note |
|---|---|---|
| the expression | transforms | the map part |
| the if clause | selects | the filter part |
| both together | transform the selected | in that order |
Recognise the map form.
Why: The expression applies an operation to every element, which is lesson 10b's map pattern.
Recognise the filter form.
Why: An if clause after the for selects which elements to keep, and the expression is just the element itself.
Note they combine.
Why: Both clauses in one comprehension filters first and transforms what survives — which is the order they run in.
Figure (svg): The state of the program after each line of Worked example mapping and filtering, drawn as a ladder with one rung per traced line
Two patterns and their combination, in a syntax designed for exactly this. That is why the form exists rather than as a general loop replacement.
Verify: Check which happens first when both appear.
Why: The filter runs before the transformation, so [1/x for x in t if x != 0] is safe where reversing the clauses would not be. That order is worth knowing, because it is what lets an if clause guard the expression.
Prediction
An expression, a for clause, and an if clause.
t = ['a', 'B', 'c', 'D']
print([s for s in t if s.isupper()])| Part | What it does | Result |
|---|---|---|
| the if clause | selects uppercase | 'B' and 'D' |
| the expression | the element itself | unchanged |
| the brackets | a new list | ['B', 'D'] |
Predict first
What does this print?
Correct: ['B', 'D'] — the if clause keeps only the uppercase elements and the expression returns each unchanged.
Why: This is the filter pattern: the expression is just s, so nothing is transformed, and the if clause decides what survives. Option D would be the result of a comprehension whose expression was s.isupper(), which maps rather than filters — the same clause used in a different position.
Worked example
An unusually strong warning about a feature it recommends.
# 'list comprehensions are harder to debug because
# you can't put a print statement inside the loop.'
# the loop version, with a print:
for s in t:
print(s) # possible
res.append(s.capitalize())| Form | Can you inspect it? | Note |
|---|---|---|
| the loop | has a body | you can add statements |
| the comprehension | has an expression | you cannot |
| the consequence | no print, no intermediate check |
Note the advantages first.
Why: List comprehensions are concise and easy to read, at least for simple expressions — and they are usually faster than the equivalent for loops, sometimes much faster.
Note the disadvantage.
Why: They are harder to debug because you can't put a print statement inside the loop.
Note the recommendation.
Why: Use them only if the computation is simple enough that you are likely to get it right the first time — and for beginners that means never.
Figure (svg): Two columns weighing the advantages of list comprehensions against the book's reservation
A feature the book recommends and tells you when not to use. The final clause is deliberate, and it is worth taking as advice rather than as a joke.
Verify: Ask what simple enough means in practice.
Why: Simple enough that you would not have wanted a print inside the loop anyway — a single method call, a comparison, an arithmetic expression. Anything you would need to inspect halfway through is exactly the case the warning is about, so the test is whether you expect to get it right first time.
Trap
A comprehension with two for clauses, a filter, and a nested expression replaces a ten-line loop.
Compress the loop
Why: Comprehensions are concise, and this one is much shorter.
Nothing can be inspected partway through, so a wrong result gives no clue about which clause is at fault — and the concision that motivated it has produced something harder to read than the loop.
Keep them simple, or use a loop.
One for clause and a simple expression
Why: Which is the case the syntax was designed for.
A loop when you would want to inspect the middle
Why: Because a loop has a body and a comprehension does not.
The book's own test is whether the computation is simple enough that you are likely to get it right the first time. Anything that fails that test is exactly the code you will need to debug, and the form makes debugging hardest.
Faded example
The expression comes first.
Fill in the blanks
def capitalize_all(t):
return [s.capitalize() for s in t]
Why: The expression inside the brackets specifies the elements of the list, and the for clause indicates what sequence we are traversing. The book notes the syntax is a little awkward because the loop variable appears in the expression before we get to the definition.
Comparison
Fill the blanks. Both build the same list.
Comparison matrix
| Question | The loop | The comprehension |
|---|---|---|
| How many lines? | four | one |
| Can you print inside it? | yes — it has a body | no — it has an expression |
| Speed | slower | usually faster, sometimes much faster |
| When to prefer it? | when you might need to inspect the middle | when the computation is simple enough to get right first time |
The bottom row is the book's own rule, and the second row is the reason for it.
Explain it
The book recommends them and warns against them.
Discussion prompt
A classmate has discovered list comprehensions and is rewriting every loop as one. Give them the book's own advice.
Hint: What can you not do inside one?
Answer:
They are concise, easy to read for simple expressions, and usually faster than the equivalent for loops — sometimes much faster. So the enthusiasm is not misplaced.
But they are harder to debug, because you can't put a print statement inside the loop. There is no body to add anything to, so a wrong result offers no way to look partway through.
The book's rule is to use them only if the computation is simple enough that you are likely to get it right the first time — and it adds, for beginners that means never. Rewriting every loop fails that test by definition, since some of those loops were hard enough to need thinking about.
Section
Section 3
Concept
Generator expressions are similar to list comprehensions, but with parentheses instead of square brackets.
>>> g = (x**2 for x in range(5))
>>> g
<generator object <genexpr> at 0x7f4c45a786c0>
>>> next(g)
0
>>> next(g)
1| Expression | What happens | Note |
|---|---|---|
| the parentheses | a generator object | not a list |
| printing it | an object description | no values yet |
| next(g) | the next value | computed on demand |
The result is a generator object that knows how to iterate through a sequence of values. But unlike a list comprehension, it does not compute the values all at once; it waits to be asked.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 185-185
Picture it
One bracket's difference, and a different kind of object.
Figure (svg): Two columns contrasting a list comprehension with a generator expression
Which makes it the same kind of thing as a zip object from lesson 12b — an iterator, with the same restrictions.
Worked example
Asking for values one at a time.
>>> g = (x**2 for x in range(5))
>>> next(g)
0
>>> next(g)
1
>>> for val in g:
... print(val)
4
9
16
>>> next(g)
StopIteration| Step | What happens | Note |
|---|---|---|
| two calls to next | 0 and 1 | the first two values |
| the for loop | picks up where next left off | 4, 9, 16 |
| next again | exhausted | StopIteration |
Get values with next.
Why: The built-in function next gets the next value from the generator.
Note that the loop continues from there.
Why: The generator object keeps track of where it is in the sequence, so the for loop picks up where next left off.
Note the exhaustion.
Why: When you get to the end of the sequence, next raises a StopIteration exception — and once the generator is exhausted, it continues to raise it.
Figure (svg): A ladder showing a generator's position advancing through its values
Five values, delivered on demand and only once. The position is part of the generator's state, which is why the loop starts at 4 rather than at 0.
Verify: Compare with the list version.
Why: A list comprehension could be looped over repeatedly and indexed, and would have computed all five values immediately. The generator gives each up once and remembers nothing afterwards — which is the same one-pass restriction as a zip object, for the same reason.
Prediction
Parentheses rather than brackets.
g = (x**2 for x in range(5))
print(g)| Part | What is true | Note |
|---|---|---|
| the parentheses | a generator expression | not a list |
| no values computed | it waits to be asked | |
| printing | an object description |
Predict first
What does this print?
Correct: <generator object <genexpr> at 0x...> — the values have not been computed, so there is nothing to show.
Why: Unlike a list comprehension, it does not compute the values all at once; it waits to be asked. Option B is what square brackets would give, and option D is what people expect from the parentheses — but a generator expression produces a generator, not a tuple.
Worked example
Rarely with next, and often with a function.
>>> sum(x**2 for x in range(5))
30
# no brackets needed when it is the only argument
# and no list is built at any point| Part | What it does | Note |
|---|---|---|
| the generator | produces values on demand | one at a time |
| sum | consumes them | adding as it goes |
| no list | ever exists | which is the saving |
Note the common usage.
Why: Generator expressions are often used with functions like sum, max and min.
Note the syntax.
Why: When the generator is the only argument, the parentheses of the call serve as its own — no extra pair is needed.
Note what is saved.
Why: The values are consumed as they are produced, so no list of five million squares is ever built for a range of five million.
Figure (svg): The state of the program after each line of Worked example where generators are actually used, drawn as a ladder with one rung per traced line
30, without a list ever existing. That is the practical case for generators: the same answer with the memory of one value rather than all of them.
Verify: Ask when the saving matters.
Why: Not for five values, and decisively for a very large range — where the list version would build every value in memory first and the generator version never holds more than one. The difference is the same as lesson 12b's argument for zip returning an iterator.
Trap
A generator is stored, summed to get a total, and then looped over to print the values.
Treat it as a collection
Why: It behaves like one in the first pass.
The sum exhausted it, so the loop produces nothing at all — no error, and no output. Once the generator is exhausted, it continues to raise StopIteration, which a for loop reads as finished.
Use it once, or build a list.
Pass it straight to the function that consumes it
Why: Which is the common usage.
Or use a list comprehension if you need it twice
Why: One bracket's difference.
This is lesson 12b's consumed-iterator trap exactly, and the symptom is the same: a loop that works and a later identical loop that does nothing. Whenever that happens, an iterator has already been spent.
Sorting
Ask whether you need the values more than once.
Sort into buckets
For each use, which form fits?
Faded example
Parentheses, not brackets.
Fill in the blanks
total = sum(x2 for** x in range(1000000))
Why: As the only argument, the generator expression needs no parentheses of its own — the call's serve. The values are produced and consumed one at a time, so no list of a million squares is ever built, which is the whole reason to prefer the generator here.
Socratic
Two calls to next came first.
Discussion prompt
After two calls to next, a for loop over the same generator prints 4, 9 and 16. Explain why it does not start at 0.
Hint: What does the generator remember?
Answer:
Its position. The generator object keeps track of where it is in the sequence, and the two calls to next already delivered 0 and 1.
So the for loop picks up where next left off — it is not starting a new traversal, because there is only ever one traversal of a generator.
Which is what makes a generator different in kind from a list: a list has values you can visit as often as you like, and a generator has a position that only moves forward. That is why exhausting one leaves nothing behind.
Section
Section 4
Concept
Python provides a built-in function, any, that takes a sequence of boolean values and returns True if any of the values are True — and another, all, that returns True if every element of the sequence is True.
>>> any([False, False, True])
True
>>> any(letter == 't' for letter in 'monty')
True
def avoids(word, forbidden):
return not any(letter in forbidden for letter in word)| Call | What it asks | Note |
|---|---|---|
| any on a list | True if any element is | works on any sequence |
| any on a generator | the common usage | evaluated on demand |
| avoids | reads like English | almost |
The function almost reads like English: word avoids forbidden if there are not any forbidden letters in word. And using any with a generator expression is efficient because it stops immediately if it finds a True value, so it doesn't have to evaluate the whole sequence.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 185-186
Picture it
It does not have to look at everything.
Figure (svg): A flowchart showing any stopping as soon as it finds a true value
Which pairs particularly well with a generator, since the values not needed are never computed either.
Worked example
Four lines become one that reads like a sentence.
# the loop version
def avoids(word, forbidden):
for letter in word:
if letter in forbidden:
return False
return True
# with any
def avoids(word, forbidden):
return not any(letter in forbidden for letter in word)| Version | How it works | Note |
|---|---|---|
| the loop | the search pattern | return on the first hit |
| any | the same early exit | built in |
| not | inverts the question | avoids rather than contains |
Note what the loop does.
Why: It is the search pattern from chapter 9: return False on the first forbidden letter, and True if the loop finishes.
Note what any does.
Why: Exactly the same thing, including stopping at the first True — which is why the rewrite is efficient as well as short.
Note the reading.
Why: The function almost reads like English: word avoids forbidden if there are not any forbidden letters in word.
Figure (svg): The state of the program after each line of Worked example rewriting a search function, drawn as a ladder with one rung per traced line
The same algorithm with the loop replaced by a built-in. Both stop early, and only one of them says what it is doing in a sentence.
Verify: Check the empty-word case in both.
Why: An empty word gives True from both: the loop never runs and returns True, and any over an empty generator returns False, which not inverts. Agreeing on the empty case is worth checking because that is where a loop-to-builtin rewrite most often diverges.
Prediction
One of the letters matches.
print(any(letter == 't' for letter in 'monty'))| Letter | The test | Note |
|---|---|---|
| m, o, n | False each | keep going |
| t | True | stop |
| y | never evaluated |
Predict first
What does this print?
Correct: True — any returns True if any of the values are True, and the fourth letter matches.
Why: It stops immediately on finding a True value, so the final y is never tested. The book notes this example isn't very useful because it does the same thing as the in operator — the point is the pattern, which generalises to conditions the in operator cannot express.
Worked example
The complement, with the same shape.
# the exercise: rewrite uses_all with all
def uses_all(word, required):
return all(letter in word for letter in required)
# all returns True if every element is True| Part | What it does | Note |
|---|---|---|
| all | True if every value is | the complement of any |
| the generator | one boolean per required letter | |
| the reading | word uses all required letters | if every one is in it |
Note the pairing.
Why: any asks whether at least one is True; all asks whether every one is.
Write the generator.
Why: One boolean per required letter, asking whether that letter is in the word.
Note the direction.
Why: The generator iterates over required rather than over word, because the question is about every required letter.
Figure (svg): Two columns comparing any and all with their empty-sequence behaviour
One line, reading as the question it answers. The exercise is worth doing because choosing which sequence to iterate over is the only decision in it.
Verify: Check what all returns for an empty sequence.
Why: True — vacuously, since no element is False. So uses_all with no required letters reports True, which is the sensible answer and worth confirming rather than assuming, since any over an empty sequence gives False.
Trap
A check is written as any([letter in forbidden for letter in word]).
Use a comprehension, since that is the form you know
Why: The brackets are familiar and it works.
The list is built in full before any looks at it, so the early exit saves nothing — every letter is tested even though the answer was settled by the first one.
Drop the brackets.
any(letter in forbidden for letter in word)
Why: A generator expression, evaluated on demand.
So the early exit actually saves work
Why: Values after the first True are never computed.
Using any with a generator expression is efficient because it stops immediately if it finds a True value — and that efficiency depends on the values not existing yet, which the brackets destroy.
Discrimination
At least one, or every one?
Sort into buckets
For each question, which function answers it?
Faded example
Not any forbidden letters.
Fill in the blanks
def avoids(word, forbidden):
return not any(letter in forbidden for letter in word)
Why: The function almost reads like English: word avoids forbidden if there are not any forbidden letters in word. Using any with a generator expression is efficient because it stops immediately on finding a True value — which putting the expression in brackets would prevent.
Real world
Checking until you know the answer.
Discussion prompt
Think of a check where you stop as soon as you know. What would it cost to look at everything anyway?
Hint: Looking for one bad item.
Answer:
Checking whether any item in a delivery is damaged, whether any door is unlocked, whether any test failed — in each case one positive settles it and the rest is wasted looking.
The cost of continuing is proportional to how much is left, which for a large collection can be everything. Stopping early is the difference between checking one and checking all of them.
Which is why the book pairs any with a generator: the early exit only saves work if the remaining values have not already been computed. Wrapping the same expression in brackets builds them all first and throws the saving away.
Section
Section 5
Concept
Every feature in this chapter has a longer equivalent that the book taught first. The question each time is whether the shorter form is clearer, not merely shorter.
The book's framing is that these are not really necessary — you can write good code without them — but with them you can sometimes write code that's more concise, readable or efficient. Sometimes is the operative word.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 183-186
Picture it
Every one has a case where it is the wrong choice.
Figure (svg): Two columns pairing each feature with the situation that makes it the wrong choice
Which is consistent with the book's whole approach: the point is to be able to choose, not to prefer the newest thing you learned.
Worked example
Two rewrites, one of which should not be made.
# worth rewriting
res = [s.capitalize() for s in t]
# not worth rewriting: too much happens
res = [f(g(s)) for s in t if h(s) and s not in seen]
# nothing can be inspected partway through| Comprehension | Simple enough? | Note |
|---|---|---|
| one method call | simple | get it right first time |
| two functions and two conditions | not simple | and undebuggable |
| the test | would you have wanted a print? |
Apply the book's test.
Why: Use them only if the computation is simple enough that you are likely to get it right the first time.
Check the first.
Why: A single method call on each element — nothing to get wrong, and nothing you would want to print.
Check the second.
Why: Two function calls, a condition and a membership test, none of which can be inspected. If it is wrong, the only option is to rewrite it as a loop to find out why.
Figure (svg): A decision flowchart for whether to use a comprehension
One rewrite that helps and one that trades legibility and debuggability for line count. Shorter and better are different claims.
Verify: Ask what you would do if the second one were wrong.
Why: Convert it back to a loop and add prints — which means the comprehension has to be undone before it can be debugged. That round trip is the cost the book's warning is about, and it lands exactly when you are least able to afford it.
Two truths and a lie
Two are true. Keep the lie.
Eliminate the wrong options
Rule out the two true statements.
Survives elimination: C
Why: C contradicts the chapter's opening. Python provides a number of features that are not really necessary — you can write good code without them — but with them you can sometimes write code that's more concise, readable or efficient. Everything here has a longer equivalent that the book taught first.
Worked example
The chapter's forms combine naturally.
# a generator, a condition, and a built-in
total = sum(x**2 for x in t if x > 0)
# any over a generator with a conditional expression
ok = all((x if x else 1) > 0 for x in t)| Part | Which feature | Note |
|---|---|---|
| the generator | produces on demand | no list |
| the if clause | filters | as in a comprehension |
| the built-in | consumes | sum, any or all |
Note that the clauses transfer.
Why: A generator expression takes the same for and if clauses a comprehension does — only the brackets differ.
Note the natural pairing.
Why: Generator expressions are often used with functions like sum, max and min, and with any and all.
Note the limit.
Why: The second example is already at the edge of readable, which is where the book's reservation starts applying.
Figure (svg): The state of the program after each line of Worked example the features working together, drawn as a ladder with one rung per traced line
Features that compose, and compose past the point of clarity. The first line is a good use and the second is a warning.
Verify: Read the second line aloud.
Why: It does not read as a sentence, which is the test the chapter has been using for every feature in it — conditional expressions almost read like English, and avoids almost reads like English. When a combination stops doing that, the concision has stopped buying readability.
Trap
Every loop in a working program is converted to a comprehension after reading this chapter.
Apply what you have learned
Why: The new forms are shorter and often faster.
Working code is being changed for no requirement, which risks introducing bugs into things that were correct — and the loops most worth rewriting are the simple ones, where the gain is smallest.
Use them where you are writing something new.
And where the shorter form is clearer
Why: Which is the actual criterion.
Leave working code alone unless there is a reason
Why: Concision is not a requirement.
The chapter's own framing is that these features are not really necessary and you can write good code without them. Code that already works is evidence of exactly that.
Sorting
Four forms, four situations.
Sort into buckets
For each situation, which feature of this lesson applies?
Faded example
A generator with a filter.
Fill in the blanks
total = sum(x2 for x in t if** x > 0)
Why: A generator expression takes the same for and if clauses a list comprehension does — only the brackets differ, and here the call's parentheses serve. The filter runs before the expression, so only the positive values are squared and nothing is stored.
Explain it
A chapter of optional features.
Discussion prompt
A classmate wants to know which of these they should adopt. Give them the chapter's own position.
Hint: What did the book say about necessity?
Answer:
None of them is necessary — the chapter opens by saying so. Python provides a number of features that are not really necessary, and you can write good code without them.
What they buy is that you can sometimes write code that's more concise, readable or efficient, and sometimes all three. Sometimes is the operative word, and each feature has a case where it is the wrong choice.
So the useful skill is choosing, which is what the whole book has been building towards — being comfortable converting between forms so you can pick the best one for what you are doing. Adopting all of them everywhere is not choosing.
Comparison
Fill the blanks. One bracket's difference.
Comparison matrix
| Question | [x**2 for x in t] | (x**2 for x in t) |
|---|---|---|
| What is produced? | a list | a generator object |
| When are the values computed? | all at once | one at a time, when asked |
| Can you loop over it twice? | yes | no — it is exhausted |
| When is each right? | when you need the values again | when something consumes them once |
The generator has the same restrictions as a zip object from lesson 12b, for the same reason: it is an iterator.
Pattern
Five questions, and the last is the book's own.
Step 3 is the one that generalises. Every feature in this chapter was introduced with a note that it almost reads like English, and when a rewrite stops doing that, it has stopped buying readability.
Python documentation — Data Structures Data Structures
Check
The condition is in the middle.
y = 'yes' if 3 > 5 else 'no'
print(y)| Part | What happens | Result |
|---|---|---|
| 3 > 5 | False | |
| the else value | chosen | 'no' |
| the first value | not evaluated |
Check your understanding
What does this print?
Answer: A
Why: The value before if is chosen when the condition holds and the value after else otherwise — so a False condition selects 'no'. Only the selected value is evaluated, which is what lets the form guard against errors like math.log of a negative number.
Check
The book's own reservation.
Check your understanding
Why does the book recommend against list comprehensions for beginners?
Answer: A
Why: The book's advice is to use them only if the computation is simple enough that you are likely to get it right the first time — and for beginners that means never. There is no body to add a print to, so a wrong result gives no way to look partway through.
Check
The values are computed on demand.
g = (x for x in range(3))
print(list(g))
print(list(g))| Call | What happens | Result |
|---|---|---|
| the first list | walks the generator | [0, 1, 2] |
| the generator | now exhausted | |
| the second list | nothing left | [] |
Check your understanding
What does the second print show?
Answer: A
Why: The first conversion walked the generator to its end, and once exhausted it has nothing left to give. This is lesson 12b's consumed-iterator behaviour: no error, and no values — which is why a generator needed twice should be a list comprehension instead.
Real world
A shorter way of saying something, which is not always clearer.
Discussion prompt
Think of a piece of shorthand or jargon in a field you know. When does it help, and when does it obscure?
Hint: Who is reading?
Answer:
It helps between people who share it — one word carries a paragraph, and the conversation moves faster. It obscures the moment someone reading does not.
And the same shorthand can do both in one document, depending on whether the reader has the context that makes it compress rather than hide.
Which is the chapter's position exactly: these features can make code more concise, readable or efficient, and sometimes all three — with sometimes doing real work in that sentence. The test is whether the shorter form still says what it means.
Commit first
Answer, then rate your confidence.
Predict first
Why does the book say list comprehensions are harder to debug?
Correct: Because you can't put a print statement inside the loop — there is no body to add one to.
Why: This is the reservation the book attaches to a feature it otherwise recommends, and it is worth taking at face value. A comprehension is an expression, so there is nowhere to insert a statement — which means a wrong result offers no way to inspect what is happening partway through. The only recourse is to convert it back to a loop, and that round trip lands exactly when you are least able to afford it. Hence the rule: use them only if the computation is simple enough that you are likely to get it right the first time, and for beginners that means never. The advantages are real too — comprehensions are concise, easy to read for simple expressions, and usually faster than the equivalent for loops, sometimes much faster — which is why the useful test is not can I write this as a comprehension but would I have wanted a print inside the loop. If the answer is yes, write the loop.
Explain it
One bracket, two very different objects.
Discussion prompt
A classmate wrote (x**2 for x in range(5)) expecting a tuple and got something else. Explain what they have and how to get what they wanted.
Hint: Parentheses do not build a tuple here.
Answer:
They have a generator object, which knows how to iterate through a sequence of values but has not computed any of them — it waits to be asked, which is why printing it shows a description rather than contents.
Square brackets would give a list with all five values computed immediately; tuple(...) around the generator would give the tuple they expected.
And it is worth knowing which they want. A generator is right when something consumes the values once — sum, any, a single for loop — and wrong when they need to index, sort or traverse twice, because it can be walked only once.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: The conditional expression has an unusual word order and a precise rule, and the short-circuiting is what makes the book's opening example work. The comprehension is the feature most worth being cautious about, and the book's own test — would you have wanted a print inside the loop — is a good one to carry. Generators differ from comprehensions in one bracket and in everything about when the values exist. And any with a generator is the pairing that makes the early exit worth anything, which is why the brackets matter there too.
Connect it up
One page, from memory.
Draw it
Write a conditional expression and label its three parts in the order they appear. Beside it, write the same map operation as a loop and as a comprehension, and note the one thing you can do in the loop that you cannot in the comprehension. Underneath, write a comprehension and a generator expression for the same values and list three differences. Finally write avoids using any, and say why the brackets must be omitted.
Recap
Four pages, and the syntax the book deliberately saved.
| If you remember one thing | It is this |
|---|---|
| From the chapter's opening | None of this is necessary. It is sometimes better. |
| From conditional expressions | Only the selected value is evaluated. |
| From comprehensions | No body means no print. Use them where you will get it right first time. |
| From generators | One bracket's difference, and the values do not exist until asked for. |
| From any and all | Drop the brackets, or the early exit saves nothing. |
The next lesson covers the chapter's alternative containers: sets, which are collections of keys with no values; Counters, which count how many times each element appears; and defaultdict, which generates a new value on the fly when a key is missing.
Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 183-185 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.