19a Conditional Expressions, Comprehensions, and Generators

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

What this lesson covers

The lesson, slide by slide

1. Lesson 19a Conditional Expressions, Comprehensions, and Generators

Title

Python · Chapter 19 — The Goodies

§19.1-19.4, pp. 183-185

2. By the end of this lesson you can

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

3. Before we start: why were these left out?

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.

4. The one idea behind this chapter: not necessary, and sometimes better

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

One of my goals for this book has been to teach you as little Python as possible.

Think Python, 2nd edition — Allen B. Downey §19.1-19.4, pp. 183-183

5. Conditional expressions

Section

Section 1

6. Choosing one of two values, in one line

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')
PartWhere it appearsNote
the value if truecomes firstmath.log(x)
the conditionin the middleif x > 0
the value if falselastelse 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

7. Picture it: the three parts, in an unusual order

Picture it

Value, condition, alternative.

Figure (svg): A conditional expression broken into its three parts in the order they appear

The condition is in the middle, which is why it reads like a sentence rather than like an if statement.

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.

8. Worked example: the rule for when you can convert

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')
ShapeConvertible?Note
both assign to ya value eachconvertible
one assigns, one printsdifferent actionsnot convertible
the rulesimple 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

The whole run at once: each drop is one line of the program.

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.

9. Predict: what does the expression give?

Prediction

The condition is in the middle.

x = -5
y = 'positive' if x > 0 else 'not positive'
print(y)
PartWhat happensResult
the conditionx > 0 is False
the else valuechosen'not positive'
the first valuenever evaluated

Predict first

What does this print?

  • not positive
  • positive
  • False
  • -5

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.

10. Worked example: two things it improves

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
CaseWhat it choosesNote
the recursionbase case and recursive caseone line
the optional argumenta default when None was passedone line
bothassign or return one of two valuesthe 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

The same behaviour, including creating the list per call rather than once.

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.

11. Trap: nesting conditional expressions

Trap

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

The fix

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.

12. Discriminate: can this become an expression?

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?

convertible
both branches assign a value to y; both branches return a value; both branches assign a single expression to the same name
not convertible
one branch assigns, the other prints; one branch assigns to y, the other to z; a branch assigns and then calls a function
yes
Each has two simple expressions that are returned or assigned to the same variable, which is exactly the rule the book gives.
no
Each fails one half of the rule: different actions, different destinations, or more than one statement in a branch. An expression produces one value and cannot do two things.

13. Complete it: the recursive factorial in one line

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.

14. Explain it yourself: why does only one value get evaluated?

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.

15. List comprehensions

Section

Section 2

16. Map and filter, in brackets

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()]
PartWhat it specifiesNote
the bracketsa new listbeing constructed
the expressionwhat each element iss.capitalize()
the for clausewhat is traversedfor s in t
an if clausewhich elements to keepfiltering

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

17. Picture it: the four-line loop becomes one

Picture it

The same three ingredients, rearranged.

Figure (svg): Two columns comparing an accumulator loop with the equivalent list comprehension

The bracket operators indicate that we are constructing a new list.

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.

18. Worked example: mapping and filtering

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
FormWhat it doesNote
the expressiontransformsthe map part
the if clauseselectsthe filter part
both togethertransform the selectedin 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

The whole run at once: each drop is one line of the program.

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.

19. Predict: what does the comprehension produce?

Prediction

An expression, a for clause, and an if clause.

t = ['a', 'B', 'c', 'D']
print([s for s in t if s.isupper()])
PartWhat it doesResult
the if clauseselects uppercase'B' and 'D'
the expressionthe element itselfunchanged
the bracketsa new list['B', 'D']

Predict first

What does this print?

  • ['B', 'D']
  • ['A', 'B', 'C', 'D']
  • ['a', 'c']
  • [True, False, True, False]

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.

20. Worked example: the book's own reservation

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())
FormCan you inspect it?Note
the loophas a bodyyou can add statements
the comprehensionhas an expressionyou cannot
the consequenceno 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

The book gives both, and the last line is its own.

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.

21. Trap: a comprehension too complicated to check

Trap

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

The fix

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.

22. Complete it: capitalise every element

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.

23. Compare: the loop and the comprehension

Comparison

Fill the blanks. Both build the same list.

Comparison matrix

QuestionThe loopThe comprehension
How many lines?fourone
Can you print inside it?yes — it has a bodyno — it has an expression
Speedslowerusually faster, sometimes much faster
When to prefer it?when you might need to inspect the middlewhen 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.

24. Explain it: should I use comprehensions?

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.

25. Generator expressions

Section

Section 3

26. Like a comprehension, and it waits to be asked

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
ExpressionWhat happensNote
the parenthesesa generator objectnot a list
printing itan object descriptionno values yet
next(g)the next valuecomputed 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

27. Picture it: computed now, or computed later

Picture it

One bracket's difference, and a different kind of object.

Figure (svg): Two columns contrasting a list comprehension with a generator expression

It does not compute the values all at once; it waits to be asked.

Which makes it the same kind of thing as a zip object from lesson 12b — an iterator, with the same restrictions.

28. Worked example: next, and StopIteration

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
StepWhat happensNote
two calls to next0 and 1the first two values
the for looppicks up where next left off4, 9, 16
next againexhaustedStopIteration

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

The generator object keeps track of where it is in the sequence.

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.

29. Predict: what does printing the generator show?

Prediction

Parentheses rather than brackets.

g = (x**2 for x in range(5))
print(g)
PartWhat is trueNote
the parenthesesa generator expressionnot a list
no values computedit waits to be asked
printingan object description

Predict first

What does this print?

  • <generator object <genexpr> at 0x...>
  • [0, 1, 4, 9, 16]
  • 0
  • (0, 1, 4, 9, 16)

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.

30. Worked example: where generators are actually used

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
PartWhat it doesNote
the generatorproduces values on demandone at a time
sumconsumes themadding as it goes
no listever existswhich 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

The whole run at once: each drop is one line of the program.

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.

31. Trap: using a generator twice

Trap

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

The fix

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.

32. Sort: list comprehension or generator expression?

Sorting

Ask whether you need the values more than once.

Sort into buckets

For each use, which form fits?

a generator expression
summing squares over a huge range; feeding max directly; checking a condition with any
a list comprehension
a list you will index into; a result you will loop over twice; a list you will sort
gen
Each consumes the values once, immediately, and never needs them again — so building a list would be wasted work and, for a huge range, wasted memory.
lst
Each needs the values to persist: indexing, a second traversal, or sorting all require a real list, which a generator cannot provide.

33. Complete it: sum without building a list

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.

34. Think it through: why does the for loop start at 4?

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.

35. any and all

Section

Section 4

36. Two built-ins that read like English

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)
CallWhat it asksNote
any on a listTrue if any element isworks on any sequence
any on a generatorthe common usageevaluated on demand
avoidsreads like Englishalmost

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

37. Picture it: any stops early

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

It stops immediately if it finds a True value, so it doesn't have to evaluate the whole sequence.

Which pairs particularly well with a generator, since the values not needed are never computed either.

38. Worked example: rewriting a search function

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)
VersionHow it worksNote
the loopthe search patternreturn on the first hit
anythe same early exitbuilt in
notinverts the questionavoids 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 whole run at once: each drop is one line of the program.

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.

39. Predict: what does any report?

Prediction

One of the letters matches.

print(any(letter == 't' for letter in 'monty'))
LetterThe testNote
m, o, nFalse eachkeep going
tTruestop
ynever evaluated

Predict first

What does this print?

  • True
  • False
  • 't'
  • 4

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.

40. Worked example: all, and the exercise

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
PartWhat it doesNote
allTrue if every value isthe complement of any
the generatorone boolean per required letter
the readingword uses all required lettersif 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

Complementary questions, and opposite answers on an empty sequence.

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.

41. Trap: building a list where a generator would do

Trap

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

The fix

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.

42. Discriminate: any or all?

Discrimination

At least one, or every one?

Sort into buckets

For each question, which function answers it?

any
does the word contain a forbidden letter?; is at least one score above the threshold?; is any element negative?
all
does the word use every required letter?; are all the scores above the threshold?; are all the elements positive?
any
Each asks whether at least one thing is true, which is what any answers — and it stops at the first True it finds.
all
Each asks whether every one is true, which is what all answers — and it stops at the first False.

43. Complete it: avoids, in one line

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.

44. Where stopping early matters

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.

45. Choosing when to use these

Section

Section 5

46. Not necessary, and the judgement is yours

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

47. Picture it: each feature's condition

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

The chapter recommends all four and gives a reservation about each.

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.

48. Worked example: when the shorter form is worse

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
ComprehensionSimple enough?Note
one method callsimpleget it right first time
two functions and two conditionsnot simpleand undebuggable
the testwould 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 question, and it is the book's own test in another form.

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.

49. Two truths and a lie: the goodies

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. List comprehensions are usually faster than the equivalent for loops, sometimes much faster
  • B. They are harder to debug because you can't put a print statement inside the loop
  • C. These features are necessary for writing good Python

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.

50. Worked example: the features working together

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)
PartWhich featureNote
the generatorproduces on demandno list
the if clausefiltersas in a comprehension
the built-inconsumessum, 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

The whole run at once: each drop is one line of the program.

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.

51. Trap: rewriting working code to use a new feature

Trap

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

The fix

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.

52. Sort: which feature fits?

Sorting

Four forms, four situations.

Sort into buckets

For each situation, which feature of this lesson applies?

a conditional expression
choosing between two values for one variable; a default when an optional argument is None
a list comprehension
transforming every element of a list; keeping only the elements that pass a test
a generator or any/all
summing over a very large range; checking whether any element satisfies a condition
ce
Both choose one of two values and assign it to a single name, which is exactly the rule for converting a conditional statement.
lc
Both are the map and filter patterns, which is what the bracket form was designed for.
ge
Both consume values once without needing a list — one summing on demand, one stopping early at the first True.

53. Complete it: sum the positives without a list

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.

54. Explain it: should I be using all of these?

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.

55. Compare: a list comprehension and a generator expression

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 lista generator object
When are the values computed?all at onceone at a time, when asked
Can you loop over it twice?yesno — it is exhausted
When is each right?when you need the values againwhen 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.

56. The procedure: deciding whether to use a shorter form

Pattern

Five questions, and the last is the book's own.

  1. Write the longer form first, if the computation is not obvious.
  2. Check the shorter form applies at all — a conditional expression needs two simple expressions going to one place.
  3. Ask whether the shorter version reads as the question it answers, which is the chapter's recurring test.
  4. For a comprehension, ask whether you would have wanted a print inside the loop.
  5. For a generator, ask whether anything needs the values more than once.

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

57. Check yourself 1 of 3: conditional expressions

Check

The condition is in the middle.

y = 'yes' if 3 > 5 else 'no'
print(y)
PartWhat happensResult
3 > 5False
the else valuechosen'no'
the first valuenot evaluated

Check your understanding

What does this print?

  • A. no (correct)
  • B. yes
  • C. False
  • D. A SyntaxError

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.

Why B tempts people
This would require the condition to be True, and three is not greater than five.
Why C tempts people
The condition's value is not the result; it chooses between the two values.
Why D tempts people
The syntax is legal: value, if, condition, else, value.

58. Check yourself 2 of 3: comprehensions

Check

The book's own reservation.

Check your understanding

Why does the book recommend against list comprehensions for beginners?

  • A. They are harder to debug, because you can't put a print statement inside the loop (correct)
  • B. They are slower than the equivalent for loops
  • C. They do not work on all sequence types
  • D. They cannot filter

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.

Why B tempts people
The opposite: they are usually faster, sometimes much faster, which is one of their advantages.
Why C tempts people
They work on anything iterable, exactly as the equivalent loop would.
Why D tempts people
An if clause after the for filters, which is one of the two patterns the section covers.

59. Check yourself 3 of 3: generators

Check

The values are computed on demand.

g = (x for x in range(3))
print(list(g))
print(list(g))
CallWhat happensResult
the first listwalks the generator[0, 1, 2]
the generatornow exhausted
the second listnothing left[]

Check your understanding

What does the second print show?

  • A. [] (correct)
  • B. [0, 1, 2]
  • C. A StopIteration exception
  • D. None

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.

Why B tempts people
A generator has one traversal, not a repeatable collection of values.
Why C tempts people
list handles the StopIteration internally and returns what it collected, which here is nothing.
Why D tempts people
list always returns a list, even an empty one.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

Commit first

Answer, then rate your confidence.

Predict first

Why does the book say list comprehensions are harder to debug?

  • Because you can't put a print statement inside the loop — there is no body to add one to
  • Because they run more slowly, so errors take longer to find
  • Because they cannot be used with an if clause
  • Because they only work on lists

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • Conditional expressions, and the rule for when a statement converts
  • List comprehensions, and the book's reservation about them
  • Generator expressions, and how they differ from comprehensions
  • any and all, and pairing them with a generator

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Four pages, and the syntax the book deliberately saved.

If you remember one thingIt is this
From the chapter's openingNone of this is necessary. It is sometimes better.
From conditional expressionsOnly the selected value is evaluated.
From comprehensionsNo body means no print. Use them where you will get it right first time.
From generatorsOne bracket's difference, and the values do not exist until asked for.
From any and allDrop 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

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §19.1-19.4, pp. 183-185
  2. Python documentation — Data Structures
  3. Python documentation — Expressions

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108