This lesson meets fibonacci, whose double recursion cannot be traced by hand, then diagnoses an infinite recursion caused by a non-integer argument and fixes it with the guardian pattern, and closes with the three possibilities to consider when a function is not working.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 6 — Fruitful functions
§6.7-6.9, pp. 57-59
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 57-59 — the pages these objectives are drawn from
Warm-up
You traced countdown and factorial by hand. Estimate the limit.
Discussion prompt
factorial(3) made three recursive calls, in a single chain. Suppose a function made TWO recursive calls each time instead of one. How many calls would there be by the fourth level, and could you still follow it on paper?
Hint: Each level doubles.
Answer:
One call becomes two, then four, then eight — so a depth of four is already fifteen calls, and it is a tree rather than a chain.
Tracing it on paper is possible and unpleasant, and by n equal to 10 there are hundreds of calls. The book's phrasing for this is that your head explodes.
That is exactly why the leap of faith exists. This lesson meets the function that requires it, and then meets the failure that the leap of faith would not have caught.
Concept
Two things make a recursive function trustworthy. The leap of faith checks that each level is correct given the one below it. A guardian checks that the input is the kind of thing the base case can catch — and without the second, the first proves nothing.
guardian — A conditional at the start of a function that protects the code after it from values that would cause an error.
The book names this pattern at the end of the factorial discussion: the first two conditionals act as guardians, protecting the code that follows from values that might cause an error, and the guardians make it possible to prove the correctness of the code.
Figure (svg): A flow chart showing two guardian checks rejecting bad inputs before the base case and recursive case are reached
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 58-59
Section
Section 1
Concept
After factorial, the most common example of a recursively defined mathematical function is fibonacci — and unlike factorial it makes two recursive calls, which changes everything about how you read it.
def fibonacci(n):
if n == 0:
return 0
elif n == 1:
return 1
else:
return fibonacci(n-1) + fibonacci(n-2)| Case | What it does | Note |
|---|---|---|
| fibonacci(0) | the first base case | 0 |
| fibonacci(1) | the second base case | 1 |
| fibonacci(n) | the sum of the two before it | two recursive calls |
The definition it comes from has three lines, and the function has three branches: fibonacci of 0 is 0, fibonacci of 1 is 1, and fibonacci of n is fibonacci of n minus 1 plus fibonacci of n minus 2. The translation is as mechanical as factorial's was.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 57-58
Picture it
Each call spawns two more, so the calls branch instead of descending in a line.
Figure (svg): A call diagram showing fibonacci of four calling fibonacci of three and fibonacci of two, each of which calls two more
factorial made one call per level and the calls formed a chain. Two calls per level makes a tree, and a tree of depth n has roughly two to the n nodes — which is why tracing stops being practical almost immediately.
Worked example
Three checks, none of which involves tracing. This is the previous lesson's method, and here it is the only option.
# check 1: the base cases
# fibonacci(0) = 0 and fibonacci(1) = 1, both by definition
# check 2: smaller inputs
# n-1 and n-2 are both smaller than n
# check 3: the leap
# assuming both calls are right, is their sum right? Yes, by definition| Check | What it establishes | Effort |
|---|---|---|
| base cases | verified directly | no assumption needed |
| smaller inputs | both calls decrease n | termination |
| the leap | assume both work, check the sum | one line of reasoning |
Verify both base cases by inspection.
Why: Two of them here rather than one, and each comes straight from the definition. Neither involves a recursive call.
Check that both calls shrink the input.
Why: n-1 and n-2 are both smaller. Two recursive calls need two checks, and both pass.
Take the leap once, covering both calls.
Why: If you assume that the two recursive calls work correctly, then it is clear that you get the right result by adding them together.
Figure (svg): The state of the program after each line of Worked example reading fibonacci with the leap of faith, drawn as a ladder with one rung per traced line
Correct, in three short checks. The book's own verdict on the alternative is that if you try to follow the flow of execution here, even for fairly small values of n, your head explodes.
Verify: Compare the effort with tracing fibonacci(10).
Why: Tracing would involve about 177 calls; the leap of faith took three sentences. That gap is the argument for the technique, and it is why the book introduces the leap of faith in the section immediately before this one.
Prediction
Use the definition rather than tracing the calls.
Predict first
Given that fibonacci(4) is 3 and fibonacci(3) is 2, what is fibonacci(5)?
Correct: 5 — the sum of the two before it, 3 plus 2.
Why: This is the leap of faith applied at the arithmetic level: given the two smaller answers, this level's answer follows by one addition. Notice that you did not need to know how fibonacci(4) was computed, only what it is. That is exactly the move the technique formalises.
Worked example
Try removing one and see which inputs break.
def fibonacci(n):
if n == 0:
return 0
else:
return fibonacci(n-1) + fibonacci(n-2)| Call | What happens | Consequence |
|---|---|---|
| fibonacci(1) | not 0, so recurse | calls fibonacci(0) and fibonacci(-1) |
| fibonacci(-1) | not 0, so recurse | calls fibonacci(-2) and fibonacci(-3) |
| result | the arguments go negative and never hit 0 | infinite recursion |
Remove the n == 1 base case and trace one step.
Why: fibonacci(1) takes the recursive branch, calling fibonacci(0) and fibonacci(-1).
Follow the second call.
Why: fibonacci(-1) is not zero, so it recurses to -2 and -3, and every subsequent argument is further from zero.
Identify the failure.
Why: This is lesson 5c's second failure mode: a base case that exists and is unreachable from some inputs. The n-2 step can jump over zero.
Figure (svg): Two columns comparing fibonacci with two base cases against a version with only one, showing where the second fails
Both base cases are needed because the n-2 step can skip past zero. With only the zero case, any odd starting value produces arguments that go negative and never return to it.
Verify: Check whether widening the base case to n <= 0 would fix it.
Why: It would prevent the infinite recursion but give wrong answers: fibonacci(1) would become fibonacci(0) + fibonacci(-1), which with that base case is 0 + 0, not 1. So the second base case is needed for correctness as well as termination — which is why the definition states both.
Trap
Asked whether fibonacci is correct, a student starts drawing the call tree for n equal to 6.
Apply the method that worked for factorial
Why: Tracing settled countdown and factorial, so it seems like the way to check any recursion.
Twenty-five calls later the drawing is unreadable, and it would need hundreds more for a value worth caring about. The book's diagnosis is that your head explodes.
Use the leap of faith, which is exactly what it is for.
Check the base cases directly, since they involve no recursion
Why: Two of them here, and both are one line of the definition.
Assume both recursive calls work, and check only the addition
Why: According to the leap of faith, if you assume that the two recursive calls work correctly, then it is clear that you get the right result by adding them together.
Tracing is a tool for understanding a mechanism, not for verifying a function. Once you have traced one recursion to see how it works, the leap of faith is what you use on every subsequent one.
Estimation
Each level roughly doubles. Estimate the order of magnitude.
Predict first
Roughly how many calls does the recursive fibonacci make to compute fibonacci(10)?
Correct: About 180 — the call count grows roughly like the Fibonacci numbers themselves, and it is 177 for n equal to 10.
Why: Because each call makes two more, the total roughly doubles for every step of n, which is exponential growth. Ten levels of near-doubling gives hundreds rather than tens. This is worth estimating once because it explains both why tracing is hopeless and why this implementation is genuinely slow — a point the book returns to in chapter 11, where memos fix it.
Ranking
It is a chain, so order matters.
Put in order
Why: The two base cases are checked first, in either order relative to each other, and the recursive case must come last because it is the else. Putting the recursive case first would make both base cases unreachable and give an infinite recursion — the same unreachable-branch problem as lesson 5b's ordering rule, with a much worse consequence.
Explain it to yourself
Both are recursive. One is traceable and one is not.
Discussion prompt
Explain what changes when a function makes two recursive calls instead of one, and why that makes tracing impractical while leaving the leap of faith exactly as easy.
Hint: Count the calls at each level in each case.
Answer:
With one call per level the calls form a chain: n levels means n calls, and tracing is linear work. With two per level they form a tree, and the number of calls roughly doubles each level.
So tracing goes from n steps to about two to the n steps, which becomes impossible almost immediately.
The leap of faith is unaffected, because it never looks at more than one level. Two recursive calls means two things to assume instead of one, and the check is still a single line of reasoning about the addition. A method whose cost does not grow with the depth is the only kind that survives here.
Section
Section 2
Concept
What happens if we call factorial and give it 1.5 as an argument? It looks like an infinite recursion — and the function has a base case, so the question is how that can be.
>>> factorial(1.5)
RuntimeError: Maximum recursion depth exceeded| Call | What happens | Consequence |
|---|---|---|
| n = 1.5 | not equal to 0, so recurse | calls factorial(0.5) |
| n = 0.5 | not equal to 0, so recurse | calls factorial(-0.5) |
| n = -0.5 | gets smaller, more negative | never equals 0 |
The function has a base case — when n equals 0 — but if n is not an integer, we can miss the base case and recurse forever. In the first recursive call the value of n is 0.5; in the next it is -0.5; from there it gets smaller and more negative, but it will never be 0.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 58-58
Picture it
The argument passes zero without ever landing on it.
Figure (svg): A number line style diagram showing the arguments one point five, zero point five, minus zero point five stepping past zero
This is lesson 5c's second failure — a base case that exists and is unreachable — with a cause that is neither a wrong step nor a wrong condition, but a wrong type of argument.
Worked example
Nothing is wrong with the base case or the step. Find what is.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)| Argument kind | What the sequence does | Outcome |
|---|---|---|
| integer argument | n reaches 0 exactly | terminates |
| fractional argument | n takes fractional values forever | never reaches 0 |
| the cause | an exact-equality test on a value that never lands there | a type problem |
Check the base case in isolation.
Why: n == 0 is correct: the factorial of zero is one. Nothing about it is wrong.
Check the step in isolation.
Why: n-1 strictly decreases, so the argument does move. Nothing about it is wrong either.
Find the assumption they share.
Why: Both are correct only if n starts as a whole number. Subtracting one from an integer eventually lands on zero; subtracting one from 1.5 never does.
Figure (svg): The state of the program after each line of Worked example why the base case is missed, drawn as a ladder with one rung per traced line
The base case and the step are each correct, and together they assume an integer argument. A fractional one steps past zero forever, which is an infinite recursion caused by a type rather than by a mistake in the logic.
Verify: Ask whether widening the base case to n <= 0 would fix it.
Why: It would terminate — but factorial(1.5) would then return 1.5 * 0.5 * 1, which is not the factorial of 1.5 in any meaningful sense. Terminating is not the same as being correct, and this is exactly why the book chooses to reject the input rather than to widen the test.
Prediction
An integer argument, and still a problem.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)| Call | What happens | Outcome |
|---|---|---|
| n = -2 | not 0, so recurse | calls factorial(-3) |
| n = -3 | not 0, so recurse | calls factorial(-4) |
| the sequence | moves away from zero | never terminates |
Predict first
What happens for factorial(-2)?
Correct: Infinite recursion — subtracting one from a negative number moves it further from zero, so the base case is never reached.
Why: This is a different failure from the fractional one and it has the same cause: an exact-equality base case that the step never lands on. Here the argument is an integer, so the type is right and the SIGN is wrong. That is why the book's fix has two guardians rather than one — a type check and a sign check catch two genuinely different bad inputs.
Worked example
There is a real alternative here. Understand the choice.
# option 1: generalize factorial to floating-point numbers
# this is the gamma function -- beyond the scope of the book
# option 2: make factorial check the type of its argument
# reject anything that is not a non-negative integer| Option | What it does | Cost |
|---|---|---|
| option 1 | accept more inputs, compute correctly for all | mathematically hard |
| option 2 | accept fewer inputs, reject the rest clearly | a few lines |
| the choice | option 2 | the book's, and the practical one |
State the first option honestly.
Why: We can try to generalize the factorial function to work with floating-point numbers. That is called the gamma function, and it is a little beyond the scope of the book.
State the second.
Why: Or we can make factorial check the type of its argument, and refuse the ones it cannot handle.
Notice what the second option really achieves.
Why: It does not make the function more capable. It makes it HONEST about what it can do, which is what turns an infinite recursion into a clear message.
Figure (svg): A panel comparing the recursion depth traceback with a clear message naming the real problem
The book takes the second option. Checking the argument does not extend what the function can compute; it converts a silent, catastrophic failure into an immediate and informative one.
Verify: Compare the two failure modes from the caller's point of view.
Why: Without the check, a fractional argument produces a thousand-frame traceback about recursion depth, which says nothing about the real cause. With it, the message names the actual problem in one line. The computation is no more capable and the function is far more usable.
Trap
factorial is tested on 0, 1, 3 and 5, all of which work, and declared correct.
Test with the inputs the function was designed for
Why: They are the realistic ones, and all of them pass.
The function is correct for those and catastrophic for a fractional or negative argument — and nothing in the code says which inputs are legal.
Ask which inputs the base case can actually catch, not which ones you intend to pass.
For an exact-equality base case, ask what sequence the step generates
Why: Subtracting one from an integer hits zero. From a float it never does, and from a negative it moves away.
Then either widen the base case, or reject the input
Why: The choice depends on whether the wider case has a correct answer, which for a fractional factorial it does not.
This is the second of lesson 5c's two checks — is the base case reachable from every input — taken seriously. The point of every is that it includes inputs nobody intended to pass.
Discrimination
Ask whether repeatedly subtracting one lands exactly on zero.
Sort into buckets
For the unguarded factorial with base case n == 0, does each argument terminate?
Counterexample
The float 2.0 terminates. Ask what that means for the guardian.
Discussion prompt
factorial(2.0) terminates correctly and returns 2.0, yet an isinstance check for int would reject it. Is the guardian therefore too strict? Argue both ways.
Hint: Consider what happens with 2.0000001.
Answer:
Too strict, in one sense: 2.0 is a whole number and the function handles it fine, so rejecting it refuses a legal input.
Not too strict, in the sense that matters: floats that LOOK whole are not reliably whole. A value computed as 2.0000000001 would be rejected by no simple test and would recurse forever, and distinguishing the safe floats from the dangerous ones is harder than requiring an int.
So the guardian trades a small amount of generality for a guarantee. That is the usual shape of such a trade, and it is why the book says the guardians make it possible to PROVE the correctness of the code — a check that is easy to state is one you can reason about.
Socratic
Changing == to <= would stop the infinite recursion. Say why the book does not.
Discussion prompt
Replacing n == 0 with n <= 0 would make factorial terminate for every argument. Explain why that is not a fix.
Hint: Terminating and being correct are different things.
Answer:
It terminates and gives wrong answers. factorial(-2) would return -2 * -3 * ... down to whatever value first satisfies the condition — a number with no meaning.
factorial(1.5) would return 1.5 * 0.5 * 1, which is 0.75, and the actual gamma-function value is about 1.33. So the function would confidently produce a wrong number instead of failing.
That is a semantic error in lesson 2b's sense, and it is strictly worse than the infinite recursion, which at least announced itself. Rejecting an input you cannot handle correctly is better than handling it incorrectly.
Section
Section 3
Concept
We can use the built-in function isinstance to verify the type of the argument, and while we are at it we can also make sure the argument is positive. The two checks go at the top, before anything else.
def factorial(n):
if not isinstance(n, int):
print('Factorial is only defined for integers.')
return None
elif n < 0:
print('Factorial is not defined for negative integers.')
return None
elif n == 0:
return 1
else:
return n * factorial(n-1)| Input | Which branch | What happens |
|---|---|---|
| not an int | the first guardian | message, return None |
| negative | the second guardian | message, return None |
| zero | the ordinary base case | return 1 |
| otherwise | a non-negative integer, guaranteed | recurse safely |
This program demonstrates a pattern sometimes called a guardian. The first two conditionals act as guardians, protecting the code that follows from values that might cause an error — and if we get past both checks, we know that n is a non-negative integer, so we can prove that the recursion terminates.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 58-59
Picture it
Past the second check, the possible values of n have been narrowed to exactly the ones the recursion can handle.
Figure (svg): A diagram showing any value entering, the two guardians filtering it, and only non-negative integers reaching the recursion
That narrowing is the point. The guardians do not make the recursion better; they make a statement about it provable.
Worked example
Two checks, two different bad inputs. Test each.
>>> print(factorial('fred'))
Factorial is only defined for integers.
None
>>> print(factorial(-2))
Factorial is not defined for negative integers.
None| Input | Which guardian catches it | Result |
|---|---|---|
| 'fred' | not an int | caught by the first guardian |
| -2 | an int, but negative | caught by the second |
| both | print a message and return None | to indicate something went wrong |
Notice that the two checks catch different things.
Why: The first handles non-integers; the second handles negative integers. Neither would catch the other's case.
Notice the order.
Why: The type check must come first, because n < 0 on a string would itself be an error. A guardian often protects the guardian below it.
Notice what they return.
Why: In both cases the program prints an error message and returns None to indicate that something went wrong.
Figure (svg): The state of the program after each line of Worked example what each guardian rejects, drawn as a ladder with one rung per traced line
Two guardians, two categories of bad input, each with its own message. Both return None to signal that no meaningful answer was produced.
Verify: Try reversing the order of the two guardians.
Why: With the sign check first, factorial('fred') would evaluate 'fred' < 0 and raise a TypeError before the type check ran. The order is not stylistic: each guardian assumes the ones above it have already passed, which is exactly what makes the chain safe.
Prediction
It prints something and returns something. Both matter.
>>> print(factorial('fred'))| Step | What happens | Output |
|---|---|---|
| the guardian | isinstance('fred', int) is False | the first branch runs |
| displays the message | Factorial is only defined for integers. | |
| return None | the call produces None | print displays None |
Predict first
How many lines does this display?
Correct: Two — the guardian's message, and then None, which the outer print displays because that is what the call returned.
Why: Two separate things happen: the function prints its own message as a side effect, and it returns None, which the surrounding print then displays. This is worth seeing because it shows that returning None is a real return value that the caller receives, rather than a way of returning nothing at all.
Worked example
The book's claim is about proof rather than convenience. Follow it.
# past both guardians, we know:
# n is an int (guardian 1)
# n >= 0 (guardian 2)
# therefore:
# n-1 is an int, and one step closer to 0
# repeated subtraction reaches 0 exactly
# the base case n == 0 is reached| Point in the function | What is known about n | What follows |
|---|---|---|
| before the guardians | n could be anything | no guarantee |
| after them | n is a non-negative integer | a strong guarantee |
| consequence | the recursion provably terminates | not merely usually |
State what is unknown at the top.
Why: Before the guardians, n could be a string, a float, a negative number or anything else. No claim about the recursion could be made.
State what is known after them.
Why: If we get past both checks, we know that n is a non-negative integer.
Draw the conclusion.
Why: From that, the recursion provably terminates: subtracting one from a non-negative integer repeatedly reaches zero exactly, which is the base case.
Figure (svg): A flow chart showing the two guardians narrowing the possible inputs before the base case and recursive case
The guardians turn this usually works into this always works, and here is why. The guarantee is about what remains possible after the checks, and that is what the proof rests on.
Verify: Ask what the proof would be missing without the guardians.
Why: Without them, the termination argument has to begin if n is a non-negative integer, which is an assumption about the caller rather than a fact about the function. Guardians move that condition from a hope into a check, which is the whole difference between a precondition you documented and one you enforced.
Trap
A student writes the sign check first, reasoning that it is the simpler test.
Order the checks by how easy they are rather than by what they depend on
Why: Both look like independent tests of the same value.
Then factorial('fred') evaluates 'fred' < 0, which raises a TypeError — the very kind of failure the guardians were added to prevent.
Order guardians so that each one is safe given the ones above it.
Check the type first
Why: Every later check assumes it is dealing with a number, and that assumption has to be established before it is used.
Then check the value
Why: n < 0 is meaningful only once n is known to be a number.
This is the same first-true-branch reasoning as lesson 5b, with an extra consideration: in a guardian chain, later branches depend on earlier ones having failed, so the order carries meaning beyond which case wins.
Ranking
Each branch assumes the ones above it have failed.
Put in order
Why: The type check must be first, because every later test assumes n is a number. The sign check comes next, because the base case and the recursion both assume non-negativity. Then the base case, then the recursive case — and by the time the last branch is reached, n is guaranteed to be a positive integer, which is exactly what makes the recursion provably terminate.
Faded example
Reject a divisor of zero before it can cause an error.
Fill in the blanks
def divide(a, b):
if b == 0:
print('Cannot divide by zero.')
return None
return a / b
Why: The guardian catches the one input the division cannot handle, and returns None with a message rather than letting a ZeroDivisionError propagate. Note the structure: the guardian returns early, so the division below it can be written without any further checking — which is exactly what a guardian is for. Chapter 14 shows a more flexible alternative to printing a message, which is raising an exception.
Real world
The pattern is much older than programming.
Discussion prompt
Think of a process that checks something about you before letting you proceed — a form, a door, an application. What does the check make possible for everything after it, and what happens if the checks are done in the wrong order?
Hint: Consider a form that asks for your date of birth and then whether you are over 18.
Answer:
The check lets everything downstream assume something. A door that checks a pass means every room beyond it can assume the person belongs there, and no further checking is needed.
Wrong order breaks it in the same way as in code: asking whether somebody is over 18 before establishing that they gave a date at all produces nonsense, because the second question assumes the first was answered.
The programming version adds a useful phrase for it: past the guardians, you KNOW something, and that knowledge is what makes the code after them provable rather than merely likely to work.
Section
Section 4
Concept
When a guardian rejects an input, the function prints an error message and returns None to indicate that something went wrong. That is a deliberate choice with consequences for the caller.
>>> print(factorial(-2))
Factorial is not defined for negative integers.
None
>>> factorial(-2) + 1
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'| What happens | Who it informs | Note |
|---|---|---|
| the message | printed by the function | tells a human |
| None | returned to the caller | tells the program |
| using it | None in arithmetic | TypeError, at the call site |
This works and it has a real limitation: a caller who ignores the return value gets no warning until the None is used, and the resulting error appears at the call site rather than in the function. The book flags the better alternative: in section 11.4 we will see a more flexible alternative to printing an error message, which is raising an exception.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 58-59
Picture it
The message goes to a person; the None goes to the program.
Figure (svg): Two columns showing the printed message going to a human reader and the returned None going to the calling code
The limitation is that a program has to remember to check. Chapter 14's exceptions remove that requirement, which is why the book promises them here.
Worked example
The point of returning None is that a caller CAN respond. Do so.
result = factorial(n)
if result is None:
print('could not compute a factorial for', n)
else:
print('the answer is', result)| Input | What comes back | Which branch |
|---|---|---|
| n = 5 | factorial returns 120 | the else branch |
| n = -2 | factorial prints a message and returns None | the if branch |
| the caller | can distinguish the two cases | because None is a value |
Store the result rather than using it directly.
Why: You cannot test a value you have already put into an expression.
Test for None explicitly.
Why: Comparing with is None is the idiomatic check, and it distinguishes no answer from any real answer.
Handle both cases.
Why: The caller now behaves sensibly for a bad input instead of crashing several lines later.
Figure (svg): The state of the program after each line of Worked example a caller that checks, drawn as a ladder with one rung per traced line
The caller checks the return value and takes a different path when it is None. That is only possible because the failure was communicated as a value rather than only as a printed message.
Verify: Compare with a caller that does not check.
Why: Writing print(factorial(-2) + 1) gives a TypeError about NoneType, several lines from the actual problem. The check turns a crash into a handled case, and the difference is entirely in the caller.
Prediction
The guardian rejects the input. Follow what the caller does with the result.
total = factorial(-1) + factorial(3)| Sub-expression | What it produces | Result |
|---|---|---|
| factorial(-1) | rejected: message printed, returns None | None |
| factorial(3) | computed normally | 6 |
| None + 6 | adding None to an int | TypeError |
Predict first
What happens when this line runs?
Correct: A TypeError — the first call returns None, and None cannot be added to an integer.
Why: The message was printed and the program carried on, because printing does not stop anything. The None then reached an addition and failed there. Note where the traceback points: at this line, not at the guardian that produced the None — which is why the book calls exceptions a more flexible alternative and introduces them in chapter 14.
Worked example
Suppose the guardian printed a message and returned nothing at all. Work out what breaks.
# hypothetical: guardian prints but does not return None explicitly
def factorial(n):
if not isinstance(n, int):
print('Factorial is only defined for integers.')
elif n == 0:
return 1
else:
return n * factorial(n-1)| Aspect | Effect | Note |
|---|---|---|
| what changes | the explicit return None is gone | the branch falls off the end |
| what the caller gets | None anyway | same value |
| what is lost | the statement of intent | and the early exit |
Notice the return value is unchanged.
Why: Falling off the end of a function returns None, from lesson 6a, so the caller receives exactly the same thing.
Notice what is lost anyway.
Why: The explicit return says this is deliberate. Without it, a reader cannot tell whether the None is intended or an oversight — and lesson 6a's every-path rule exists precisely because unintended Nones are common.
Notice the structural difference.
Why: An explicit return also exits immediately, which matters as soon as there is code below the guardian that must not run.
Figure (svg): Two columns comparing an explicit return None with falling off the end of a function
The caller receives None either way. The explicit return states that the None is intentional and guarantees the early exit — both of which matter to a reader, and the second of which matters to the program.
Verify: Apply lesson 6a's every-path rule to the hypothetical version.
Why: It has a path — a non-integer argument — that reaches the end of the function without hitting a return statement, which is exactly the shape that rule warns about. Writing the return explicitly is how you distinguish a deliberate None from that mistake.
Trap
A program calls a guarded function and uses the result directly in an expression, without checking it.
Assume the message on screen is enough
Why: The function did report the problem, in words, right there.
A printed message does not stop the program. The None travels into the next expression and produces a TypeError somewhere else entirely, with a traceback that points at the wrong line.
A returned None is only useful if the caller looks at it.
Store the result and test it before using it
Why: if result is None: handle the failure. That is the whole cost.
Recognise the symptom when you forget
Why: A TypeError mentioning NoneType nearly always means a function returned None and somebody used it — which is the same diagnostic as lesson 6a's missing return.
The deeper limitation is that nothing forces the caller to check, which is why chapter 14 introduces exceptions: an unhandled exception stops the program at the point of the problem rather than letting a bad value travel.
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 is the lie, and it is the misunderstanding that makes the None dangerous. Printing is an effect on the screen and nothing more — the program continues with the None, which then causes a failure somewhere else. That gap between reporting a problem and stopping is exactly what exceptions close.
Matching
Three mechanisms, three different reaches.
Match the pairs
Why: The three differ in who is informed and whether anything is enforced. Printing informs a person and enforces nothing; returning None informs the program and relies on it to check; an exception enforces that somebody deals with it. The book uses the first two here and promises the third, which is the honest ordering — each is a step up in what it guarantees.
Edge cases
It works for factorial. Find a case where it does not.
Discussion prompt
Suppose a function looks up a value and legitimately might find None stored there. What goes wrong if it also returns None to mean not found, and what would you do instead?
Hint: Can the caller tell the two cases apart?
Answer:
The caller cannot distinguish the stored value is None from nothing was found, because both come back as the same value. Two different situations have collapsed into one answer.
The usual fixes are to return two values — a success flag and the result, which chapter 12's tuples make easy — or to raise an exception for the not-found case.
The general principle: a sentinel value only works when it cannot be a legitimate result. For factorial that is safe, since no factorial is None; for a lookup it is not. Recognising when a sentinel is unsafe is what leads people to exceptions in the first place.
Section
Section 5
Concept
Breaking a large program into smaller functions creates natural checkpoints for debugging. If a function is not working, there are three possibilities to consider — and each has its own check.
The first two are the contract from lesson 4b, restated: preconditions are the caller's responsibility and postconditions are the function's. The third is new, and it is the one that catches a correct function whose result is being thrown away or misused.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 59-59
Picture it
In, inside, or out — and each has a different check.
Figure (svg): A diagram showing arguments entering a function, the function's body, and the return value leaving, with a possible fault at each stage
Ruling out the first possibility takes one print statement and eliminates a third of the search — which is exactly the value of a taxonomy.
Worked example
Each has a specific action attached. Do them in order.
def distance(x1, y1, x2, y2):
print('args:', x1, y1, x2, y2) # check 1
dx = x2 - x1
dy = y2 - y1
result = math.sqrt(dx**2 + dy**2)
print('returning:', result) # check 2
return result| Check | What you add | What it answers |
|---|---|---|
| check 1 | print the parameters at the top | are the arguments right? |
| check 2 | print the value before returning | is the result right? |
| check 3 | look at the call site | is the return value used? |
Rule out the first possibility.
Why: Add a print statement at the beginning of the function and display the values of the parameters, and maybe their types. If they are wrong, the bug is in the caller.
Rule out the second.
Why: If the parameters look good, add a print statement before each return statement and display the return value. If possible, check the result by hand.
Rule out the third.
Why: If the function seems to be working, look at the function call to make sure the return value is being used correctly — or used at all.
Figure (svg): The state of the program after each line of Worked example the three checks, in order, drawn as a ladder with one rung per traced line
Two print statements and one look at the call site. Each check eliminates one of the three possibilities, and doing them in order means never debugging a body that was given the wrong input.
Verify: Notice that these are the same print statements incremental development used.
Why: Lesson 6a's scaffolding was print statements showing intermediate values, and these are the same idea applied after the fact. A function developed incrementally has already passed both checks, which is one reason the method saves debugging time rather than adding to it.
Definition probe
Diagnose each symptom before looking at any code.
Sort into buckets
Sort each symptom by which of the three possibilities it points to.
Worked example
The function is perfect. The bug is at the call site.
def area(radius):
return math.pi * radius**2
total = 0
area(5)
print(total)| Component | Status | Note |
|---|---|---|
| the function | correct in every respect | returns the right value |
| the call | the return value is discarded | nothing stores it |
| the output | 0 | the bug is in the caller |
Check the arguments.
Why: Five is a perfectly good radius, so the first possibility is ruled out.
Check the function.
Why: Adding a print before the return would show the correct area. The second possibility is ruled out too.
Look at the call.
Why: area(5) is called and its result is thrown away — the third possibility. Look at the function call to make sure the return value is being used correctly, or used at all.
Figure (svg): A panel listing the three possibilities with the check for each
The function is correct and its result is discarded. The exclamation in the book's phrasing — or used at all — points at exactly this, and it is the possibility people check last and should sometimes check first.
Verify: Recall the same failure from lesson 6a.
Why: That lesson showed a script calling math.sqrt(5) on its own and losing the value forever. This is the same mistake with your own function, and the fix is the same: assign the result, or use it in an expression.
Trap
A function returns the wrong answer, so the developer starts reading its body line by line.
Assume the fault is where the symptom is
Why: The function produced the wrong answer, so it seems like the obvious place.
If the arguments were wrong, the body is blameless and every line of it will look correct — which it is. The reading produces nothing and the real fault is one frame up.
Rule out the arguments first, because it is cheap and it eliminates a third of the possibilities.
Print the parameters, and their types
Why: One line, at the top of the function. Wrong values there mean the caller is at fault and the body is irrelevant.
Only then look inside
Why: And when you do, print the return value before each return so you can compare against a hand calculation.
This is the contract from lesson 4b as a debugging procedure: if the preconditions are violated the bug is in the caller, and if they are satisfied and the postconditions are not, the bug is in the function.
Prediction
A function returns None and the caller crashes.
Predict first
A function that should return a number returns None, and the caller fails with a TypeError. Which of the three possibilities should you check first?
Correct: The arguments first, because it is the cheapest check and it eliminates a third of the possibilities — though a None return usually turns out to be the second possibility.
Why: The order of the checks is about cost rather than likelihood: printing the parameters takes one line and rules out the caller entirely. If they are fine, the None almost certainly comes from a path through the function that reaches no return statement, which is lesson 6a's every-path rule. Note that the TypeError's location is misleading, exactly as lesson 5c warned — the error is discovered at the call site and caused inside the function.
Comparison
Fill the blanks. Each check is one concrete action.
Comparison matrix
| Possibility | What is wrong | The check |
|---|---|---|
| the arguments | a precondition is violated | print the parameters at the top of the function |
| the function | a postcondition is violated | print the value before each return and check it by hand |
| the return value | it is misused, or not used at all | look at the call site |
The middle column is lesson 4b's contract and the right-hand column is what turns it into a procedure. Between them they say who is at fault and how to find out.
Explain it
This is the most useful debugging advice in the chapter.
Discussion prompt
A classmate's function returns the wrong answer and they have been reading its body for twenty minutes. Give them the three possibilities and tell them which to check first and why.
Hint: The cheapest check is not the most likely cause.
Answer:
Give them the three: wrong arguments, wrong function, wrong use of the result. Then say: print the parameters at the top, before reading another line.
The reason is cost, not likelihood. One print statement either eliminates a whole third of the possibilities or finds the bug outright, and twenty minutes of reading has done neither.
Add the third possibility explicitly, because it is the one people never consider: the function may be perfect and its result discarded. Checking the call site takes five seconds and is the last thing anybody thinks to do.
Comparison
Fill the blanks. Four checks, and each catches something the others do not.
Comparison matrix
| Check | What it catches | Where it came from |
|---|---|---|
| does a base case exist? | a recursion with no exit at all | lesson 5c |
| is it reachable from every input? | factorial(1.5) and factorial(-2) | lesson 5c, and this lesson's guardians |
| the leap of faith | a wrong recursive step | lesson 6b |
| does every path return? | a branch that falls off the end and gives None | lesson 6a |
Four checks from three lessons, and a recursive function needs all four. The guardian pattern is how the second one is turned from a hope into a guarantee.
Pattern
Six steps, and the first three are about the inputs rather than the algorithm.
Step 2's ordering rule is the one that catches people: a type check must precede any check that compares the value, because comparing a string with a number is itself an error.
Python documentation — Errors and Exceptions Errors and Exceptions
Check
The base case is correct. The argument never reaches it.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)| Argument | What happens | Next argument |
|---|---|---|
| 1.5 | not 0, recurse | 0.5 |
| 0.5 | not 0, recurse | -0.5 |
| -0.5 | not 0, recurse | -1.5, and onward |
Check your understanding
Why does factorial(1.5) recurse forever?
Answer: B
Why: The sequence goes 1.5, 0.5, -0.5, -1.5 and so on, stepping straight past zero without ever equalling it. The base case exists and is correct; it is simply unreachable from a fractional starting value. This is the second of the two infinite-recursion failures from lesson 5c, with a type rather than a wrong step as the cause.
Check
One order works and the other raises an error.
Check your understanding
Why must the isinstance check come before the n < 0 check?
Answer: B
Why: With the sign check first, factorial('fred') would evaluate 'fred' < 0 and raise a TypeError before the type check ever ran — the very failure the guardians exist to prevent. Each guardian is safe only because the ones above it have already passed, so the order carries meaning beyond which case wins.
Check
The function is correct and the program is wrong.
def area(r):
return math.pi * r**2
a = 0
area(3)
print(a)| Component | Status | Note |
|---|---|---|
| the function | correct | returns the right value |
| the call | result discarded | a is never assigned |
| the output | 0 | the caller is at fault |
Check your understanding
Which of the three possibilities is the problem here?
Answer: C
Why: The argument is fine and the function returns the right value. The call discards the result, so a is never given it. This is the third possibility, and the book's phrasing points at it directly: look at the function call to make sure the return value is being used correctly, or used at all.
Real world
Guardians and the three possibilities are both general.
Discussion prompt
Think of a process that rejects bad input early rather than trying to cope with it later. What does the early rejection make possible for the rest of the process, and how does it compare with trying to handle every case?
Hint: Anything with an eligibility check at the start.
Answer:
Early rejection lets everything downstream assume something, which is exactly the guardian's benefit: past the check, the range of possible inputs has narrowed and the later steps can be simpler.
Trying to cope with everything means every later step has to handle every case, so the complexity multiplies through the whole process rather than being paid once at the front.
The book's phrasing is the one to keep: the guardians make it possible to PROVE the correctness of the code. Not to make it likely to work — to make a statement about it that is actually true, which is a stronger and more useful thing.
Commit first
Answer, then rate your confidence. This is about what the leap of faith does not cover.
Predict first
A recursive function's recursive step is correct and its base case is unreachable from some inputs. What does the leap of faith tell you?
Correct: That the recursive step is correct, and nothing about termination — those are separate checks.
Why: The leap of faith assumes the recursive call returns the right answer and then checks whether this level builds the right answer from it. It never asks whether the call will ever return. That is why factorial passed every leap-of-faith check and still recursed forever on 1.5: the step was right and the base case was unreachable. The two checks from lesson 5c and the leap from lesson 6b are three genuinely different questions, and a recursive function needs all three answered.
Explain it
The guardian pattern is the most transferable thing in this chapter.
Discussion prompt
A classmate's recursive function works for their test data and hangs on an input they did not expect. Explain the guardian pattern to them in three sentences, including why the order of the checks matters.
Hint: The third sentence is about ordering.
Answer:
Say: put conditionals at the top that reject any input the function cannot handle, each reporting the problem and returning immediately. Past those checks, you KNOW what kind of value you have.
That knowledge is what lets you prove the recursion terminates, instead of hoping the caller passes something sensible.
And order them so each check is safe given the ones above it — the type check first, because comparing a string with a number would itself blow up, which would defeat the whole purpose.
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: fibonacci is mostly an argument for a technique you already have, so if it is shaky the thing to revisit is the leap of faith rather than the function. The factorial(1.5) failure is one specific and memorable case of a general rule, and having met it once is usually enough. The guardian pattern is the most transferable idea in the chapter and worth practising on a function of your own. And the three possibilities are the highest-value debugging advice in the book so far — two print statements and a look at the call site, in that order, which is worth making a habit before the programs get longer.
Connect it up
One page, from memory.
Draw it
Draw the guarded factorial as a vertical flow with four branches, and beside each branch write what is known about n at that point. Then, to the right, list the four checks a recursive function needs — base case exists, base case reachable, leap of faith, every path returns — and draw a line from each check to the branch it verifies. Finally, at the bottom, write the three debugging possibilities and the one concrete action for each.
Recap
Three pages, and chapter 6 is finished: functions that return values, and the checks that make them trustworthy.
| If you remember one thing | It is this |
|---|---|
| From fibonacci | Two recursive calls make a tree. Tracing stops being possible; the leap of faith does not. |
| From factorial(1.5) | A base case tested with == is only reachable if the step lands on it exactly. |
| From guardians | Past the checks you KNOW something, and that is what makes the code provable. |
| From returning None | A printed message informs a person. Only a return value informs the program. |
| From debugging | Print the parameters first. It is the cheapest check and it rules out a third of the possibilities. |
Chapter 7 goes back to repetition, this time without recursion: the while statement, what makes a loop terminate, and an algorithm that computes square roots by successive approximation.
Think Python, 2nd edition — Allen B. Downey §6.7-6.9, pp. 57-59 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.