6c Checking Types and Debugging Fruitful Functions

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

What this lesson covers

The lesson, slide by slide

1. Lesson 6c Checking Types and Debugging Fruitful Functions

Title

Python · Chapter 6 — Fruitful functions

§6.7-6.9, pp. 57-59

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 §6.7-6.9, pp. 57-59 — the pages these objectives are drawn from

3. Before we start: how far can you trace?

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.

4. The one idea behind this lesson: check the level, and guard the input

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

5. fibonacci: two base cases and two recursive calls

Section

Section 1

6. The function that cannot be traced

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)
CaseWhat it doesNote
fibonacci(0)the first base case0
fibonacci(1)the second base case1
fibonacci(n)the sum of the two before ittwo 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

7. Picture it: a tree, not a chain

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.

8. Worked example: reading fibonacci with the leap of faith

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
CheckWhat it establishesEffort
base casesverified directlyno assumption needed
smaller inputsboth calls decrease ntermination
the leapassume both work, check the sumone 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

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

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.

9. Predict: what is fibonacci(5)?

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)?

  • 5
  • 6
  • 8
  • 4

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.

10. Worked example: why two base cases rather than one

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)
CallWhat happensConsequence
fibonacci(1)not 0, so recursecalls fibonacci(0) and fibonacci(-1)
fibonacci(-1)not 0, so recursecalls fibonacci(-2) and fibonacci(-3)
resultthe arguments go negative and never hit 0infinite 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

A step of n-2 can jump over zero. That is why zero alone is not enough.

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.

11. Trap: trying to trace a double recursion

Trap

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

The fix

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.

12. Estimate: how many calls does fibonacci(10) make?

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)?

  • About 10
  • About 20
  • About 180
  • About 1000

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.

13. Rank: the branches of fibonacci in the order they are checked

Ranking

It is a chain, so order matters.

Put in order

  1. if n == 0: return 0
  2. elif n == 1: return 1
  3. else: return fibonacci(n-1) + fibonacci(n-2)

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.

14. Explain it yourself: why does fibonacci need the leap of faith more than factorial did?

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.

15. An infinite recursion caused by a type

Section

Section 2

16. factorial(1.5)

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
CallWhat happensConsequence
n = 1.5not equal to 0, so recursecalls factorial(0.5)
n = 0.5not equal to 0, so recursecalls factorial(-0.5)
n = -0.5gets smaller, more negativenever 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

17. Picture it: stepping past zero by half

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

Zero is highlighted and never visited. The step of 1 from a fractional start always misses it.

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.

18. Worked example: why the base case is missed

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 kindWhat the sequence doesOutcome
integer argumentn reaches 0 exactlyterminates
fractional argumentn takes fractional values forevernever reaches 0
the causean exact-equality test on a value that never lands therea 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 whole run at once: each drop is one line of the program.

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.

19. Predict: what does factorial(-2) do?

Prediction

An integer argument, and still a problem.

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
CallWhat happensOutcome
n = -2not 0, so recursecalls factorial(-3)
n = -3not 0, so recursecalls factorial(-4)
the sequencemoves away from zeronever terminates

Predict first

What happens for factorial(-2)?

  • It returns 2, ignoring the sign
  • Infinite recursion, because the argument moves away from zero
  • It returns None immediately
  • A TypeError

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.

20. Worked example: the two options, and why the book picks one

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
OptionWhat it doesCost
option 1accept more inputs, compute correctly for allmathematically hard
option 2accept fewer inputs, reject the rest clearlya few lines
the choiceoption 2the 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.

21. Trap: assuming a recursion that terminates for your tests terminates for everything

Trap

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

The fix

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.

22. Discriminate: does factorial terminate for this argument?

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?

terminates
0; 5; 100; 2.0
infinite recursion
1.5; -3
ok
Each is a non-negative whole number, so subtracting one repeatedly lands exactly on zero. 2.0 works because it is a float whose value is whole — 2.0, 1.0, 0.0, and 0.0 == 0 is True.
no
One is fractional, so the sequence steps past zero forever; one is negative, so the sequence moves away from zero. Neither can ever satisfy an exact-equality test with zero.

23. Find the counterexample: does isinstance catch everything?

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.

24. Think it through: why not just widen the base case?

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.

25. The guardian pattern

Section

Section 3

26. Conditionals that protect the code after them

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)
InputWhich branchWhat happens
not an intthe first guardianmessage, return None
negativethe second guardianmessage, return None
zerothe ordinary base casereturn 1
otherwisea non-negative integer, guaranteedrecurse 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

27. Picture it: what the guardians guarantee

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

By the fourth box, the only possible inputs are ones the base case can catch.

That narrowing is the point. The guardians do not make the recursion better; they make a statement about it provable.

28. Worked example: what each guardian rejects

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
InputWhich guardian catches itResult
'fred'not an intcaught by the first guardian
-2an int, but negativecaught by the second
bothprint a message and return Noneto 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

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

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.

29. Predict: what does the guarded factorial return for 'fred'?

Prediction

It prints something and returns something. Both matter.

>>> print(factorial('fred'))
StepWhat happensOutput
the guardianisinstance('fred', int) is Falsethe first branch runs
printdisplays the messageFactorial is only defined for integers.
return Nonethe call produces Noneprint displays None

Predict first

How many lines does this display?

  • One
  • Two
  • None
  • A traceback

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.

30. Worked example: what the guardians make provable

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 functionWhat is known about nWhat follows
before the guardiansn could be anythingno guarantee
after themn is a non-negative integera strong guarantee
consequencethe recursion provably terminatesnot 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.

31. Trap: putting the guardians in the wrong order

Trap

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

The fix

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.

32. Rank: the branches of the guarded factorial

Ranking

Each branch assumes the ones above it have failed.

Put in order

  1. if not isinstance(n, int): reject
  2. elif n < 0: reject
  3. elif n == 0: return 1
  4. else: return n * factorial(n-1)

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.

33. Complete it: add a guardian

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.

34. Where guardians appear elsewhere

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.

35. Returning None to say that something went wrong

Section

Section 4

36. What a guarded function hands back

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 happensWho it informsNote
the messageprinted by the functiontells a human
Nonereturned to the callertells the program
using itNone in arithmeticTypeError, 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

37. Picture it: two channels for the same bad news

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

Neither channel does the other's job, which is why the book uses both.

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.

38. Worked example: a caller that checks

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)
InputWhat comes backWhich branch
n = 5factorial returns 120the else branch
n = -2factorial prints a message and returns Nonethe if branch
the callercan distinguish the two casesbecause 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 whole run at once: each drop is one line of the program.

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.

39. Predict: what does this produce?

Prediction

The guardian rejects the input. Follow what the caller does with the result.

total = factorial(-1) + factorial(3)
Sub-expressionWhat it producesResult
factorial(-1)rejected: message printed, returns NoneNone
factorial(3)computed normally6
None + 6adding None to an intTypeError

Predict first

What happens when this line runs?

  • total is 6, ignoring the failed call
  • A TypeError, because None cannot be added to an int
  • total is None
  • Nothing — the message is printed and the line is skipped

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.

40. Worked example: why printing alone would not be enough

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)
AspectEffectNote
what changesthe explicit return None is gonethe branch falls off the end
what the caller getsNone anywaysame value
what is lostthe statement of intentand 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

Identical to the caller. Very different to a reader.

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.

41. Trap: a caller that ignores the None

Trap

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

The fix

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.

42. Two truths and a lie: returning None

Two truths and a lie

Two are true. Keep the lie.

Eliminate the wrong options

Rule out the two true statements.

  • A. Returning None lets the caller distinguish failure from a real answer
  • B. A function that falls off the end also returns None
  • C. Printing an error message stops the program from continuing

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.

43. Match each way of reporting a problem to what it can do

Matching

Three mechanisms, three different reaches.

Match the pairs

  • a. print an error message
  • b. return None
  • c. raise an exception (chapter 14)
  • r1. tells a human, and the program carries on regardless
  • r2. tells the caller, if the caller remembers to look
  • r3. stops the program at the point of the problem unless somebody handles it

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.

44. Push the boundary: is None always a good failure value?

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.

45. Debugging a fruitful function: three possibilities

Section

Section 5

46. Where to look when a function is not working

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

47. Picture it: three places a fruitful function can fail

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

Check them in this order: there is no point debugging a body that was given the wrong values.

Ruling out the first possibility takes one print statement and eliminates a third of the search — which is exactly the value of a taxonomy.

48. Worked example: the three checks, in order

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
CheckWhat you addWhat it answers
check 1print the parameters at the topare the arguments right?
check 2print the value before returningis the result right?
check 3look at the call siteis 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

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

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.

49. Definition probe: which possibility is 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.

wrong arguments: a precondition
printing the parameters shows a string where a number was expected; the caller passes the arguments in the wrong order
wrong function: a postcondition
the parameters are right and the returned value is wrong; the formula inside the function has a typo
wrong use of the return value
the function returns the right value and the program still shows nothing; the function is called and its result is never assigned
args
The function received something it was not designed for. The fault is at the call site, and no amount of reading the body will reveal it.
body
The inputs were correct and the output was not, so the fault is between them. This is a violated postcondition, and it is the only case where reading the body is the right first move.
use
The function did its job and the result went nowhere useful. The fault is in the line that called it, which is why the book adds or used at all.

50. Worked example: the third possibility, which is easy to forget

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)
ComponentStatusNote
the functioncorrect in every respectreturns the right value
the callthe return value is discardednothing stores it
the output0the 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.

51. Trap: debugging the body first

Trap

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

The fix

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.

52. Predict: which check would find this?

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?

  • The arguments, since a bad input might cause it
  • The function, since something in it is returning None
  • The use of the return value, since the TypeError is at the call site
  • None of the three — this is a syntax error

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.

53. Compare: the three possibilities and their checks

Comparison

Fill the blanks. Each check is one concrete action.

Comparison matrix

PossibilityWhat is wrongThe check
the argumentsa precondition is violatedprint the parameters at the top of the function
the functiona postcondition is violatedprint the value before each return and check it by hand
the return valueit is misused, or not used at alllook 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.

54. Explain it: where do you look first?

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.

55. Compare: the checks a recursive function needs

Comparison

Fill the blanks. Four checks, and each catches something the others do not.

Comparison matrix

CheckWhat it catchesWhere it came from
does a base case exist?a recursion with no exit at alllesson 5c
is it reachable from every input?factorial(1.5) and factorial(-2)lesson 5c, and this lesson's guardians
the leap of faitha wrong recursive steplesson 6b
does every path return?a branch that falls off the end and gives Nonelesson 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.

56. The procedure: making a recursive function safe

Pattern

Six steps, and the first three are about the inputs rather than the algorithm.

  1. Write down what the function requires of its argument — the type, and any restriction on the value.
  2. Add a guardian for each requirement, in an order where every check is safe given the ones above it.
  3. Have each guardian report the problem and return a value the caller can recognise.
  4. Write the base case, and confirm it is reachable from every input the guardians let through.
  5. Write the recursive case, and check it with the leap of faith.
  6. Confirm that every path through the function reaches a return statement.

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

57. Check yourself 1 of 3: why factorial(1.5) recurses forever

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)
ArgumentWhat happensNext argument
1.5not 0, recurse0.5
0.5not 0, recurse-0.5
-0.5not 0, recurse-1.5, and onward

Check your understanding

Why does factorial(1.5) recurse forever?

  • A. Because the function has no base case
  • B. Because subtracting one from 1.5 never lands exactly on zero (correct)
  • C. Because floats cannot be multiplied
  • D. Because 1.5 is smaller than 2

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.

Why A tempts people
There is a base case: n == 0. The problem is that this particular argument never satisfies it.
Why C tempts people
Floats multiply perfectly well. No multiplication is even reached, because the recursion never returns.
Why D tempts people
Size is not the issue. factorial(2) terminates and factorial(1.5) does not, and 1.5 lies between them.

58. Check yourself 2 of 3: guardian order

Check

One order works and the other raises an error.

Check your understanding

Why must the isinstance check come before the n < 0 check?

  • A. Because isinstance is faster
  • B. Because comparing a non-number with 0 would itself raise an error (correct)
  • C. Because Python requires type checks first
  • D. Because negative numbers are more common than non-integers

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.

Why A tempts people
Speed is irrelevant here, and both checks are trivially fast. The reason is correctness.
Why C tempts people
Python has no such rule. The ordering constraint comes from what each check assumes, not from the language.
Why D tempts people
Frequency does not matter. Even if non-integers were rare, the one that arrived would crash the guardian meant to protect against it.

59. Check yourself 3 of 3: the three possibilities

Check

The function is correct and the program is wrong.

def area(r):
    return math.pi * r**2

a = 0
area(3)
print(a)
ComponentStatusNote
the functioncorrectreturns the right value
the callresult discardeda is never assigned
the output0the caller is at fault

Check your understanding

Which of the three possibilities is the problem here?

  • A. The arguments: a precondition is violated
  • B. The function: a postcondition is violated
  • C. The return value is not being used (correct)
  • D. There is no problem

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.

Why A tempts people
Three is a perfectly good radius. Printing the parameter would show exactly what was expected.
Why B tempts people
The function computes the area correctly. Printing the value before the return would confirm it.
Why D tempts people
The program prints 0 where it should print an area, so something is certainly wrong — it is just not in the function.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

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?

  • That the function is correct
  • That the recursive step is correct, and nothing about termination
  • That the function will recurse forever
  • Nothing at all

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.

62. Explain it to someone else

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.

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?

  • fibonacci, and why it must be read with the leap of faith rather than traced
  • Why a non-integer argument makes factorial recurse forever
  • The guardian pattern, including the order of the checks
  • The three possibilities when a function is not working

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Three pages, and chapter 6 is finished: functions that return values, and the checks that make them trustworthy.

If you remember one thingIt is this
From fibonacciTwo 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 guardiansPast the checks you KNOW something, and that is what makes the code provable.
From returning NoneA printed message informs a person. Only a return value informs the program.
From debuggingPrint 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

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), §6.7-6.9, pp. 57-59
  2. Python documentation — Errors and Exceptions
  3. Python documentation — Built-in Functions

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

Book on Wyzant · Text (657) 465-8108