This lesson writes functions that return True or False and names them like questions, translates a recursive mathematical definition into a recursive function that returns a value, and introduces the leap of faith as a way of reading recursion without tracing it.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 6 — Fruitful functions
§6.4-6.6, pp. 54-57
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 54-57 — the pages these objectives are drawn from
Warm-up
You have been trusting other people's functions since chapter 3. Notice that you have.
Discussion prompt
When you call math.sqrt(2), do you know how it computes the square root? Does that stop you using it — and what exactly are you relying on instead?
Hint: You are relying on something other than having read the code.
Answer:
Almost nobody knows the algorithm, and it stops nobody. You rely on the interface: it takes a number and returns its square root.
The book puts it plainly: when you call math.cos or math.exp, you do not examine the bodies of those functions. You just assume that they work because the people who wrote them were good programmers.
This lesson's last section asks you to extend that trust to your own functions, including the one you are in the middle of writing. That sounds odd, which is why it is called a leap of faith.
Concept
Functions can return booleans, which is often convenient for hiding complicated tests inside functions. The caller then reads a question rather than a calculation — and does not have to look inside to know what the answer means.
boolean function — A function that returns True or False, usually named so that it reads as a yes-or-no question.
It is common to give boolean functions names that sound like yes/no questions. is_divisible returns either True or False to indicate whether x is divisible by y — and a reader of the calling code never has to think about the modulus operator.
Figure (svg): Two columns contrasting a condition written as arithmetic with the same condition written as a named boolean function
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 54-54
Section
Section 1
Concept
Functions can return booleans, which is often convenient for hiding complicated tests inside functions. The book's example is a test for divisibility.
def is_divisible(x, y):
if x % y == 0:
return True
else:
return False| Call | What happens | Return value |
|---|---|---|
| is_divisible(6, 4) | 6 % 4 is 2, not 0 | False |
| is_divisible(6, 3) | 6 % 3 is 0 | True |
| the name | reads as a yes/no question | is 6 divisible by 3? |
The naming convention is doing real work. It is common to give boolean functions names that sound like yes/no questions, so that a call reads as the question it answers rather than as a computation whose result you have to interpret.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 54-55
Picture it
The second version returns the comparison directly. Both are correct.
Figure (svg): Two columns showing the long form of is_divisible with an if-else and the concise form returning the comparison directly
The long form is not wrong, and it is a useful stage while you are convincing yourself. The short form is what you write once you believe the comparison already produces exactly the value you want.
Worked example
The if-else is redundant. Work out exactly why.
def is_divisible(x, y):
return x % y == 0| Layer | What it produces | Note |
|---|---|---|
| x % y | arithmetic | a number |
| ... == 0 | a comparison | True or False |
| return | hands back that boolean | no if needed |
Look at what the condition already is.
Why: The result of the == operator is a boolean, so the expression x % y == 0 is already exactly True or False.
See what the if-else added.
Why: It tested that boolean and then returned True when it was True and False when it was False — which is returning it unchanged, at greater length.
Write the concise form.
Why: We can write the function more concisely by returning it directly.
Figure (svg): The state of the program after each line of Worked example returning the comparison directly, drawn as a ladder with one rung per traced line
return x % y == 0. The comparison already produces the boolean the function wants, so testing it and returning a fresh one adds nothing.
Verify: Check both versions on the same two inputs.
Why: is_divisible(6, 4) gives False and is_divisible(6, 3) gives True in both versions. Confirming that a simplification preserves behaviour on the cases you care about is the same verification step as lesson 4b's refactoring — the shorter form is only better if it is also the same.
Prediction
The comparison is already a boolean.
def is_even(n):
return n % 2 == 0
print(is_even(7))| Step | What happens | Value |
|---|---|---|
| 7 % 2 | the remainder | 1 |
| 1 == 0 | the comparison | False |
| return | hands back False | False |
Predict first
What does this print?
Correct: False — seven is odd, so the remainder is 1, and 1 is not equal to 0.
Why: The two layers from lesson 5a are both here: modulus produces a number and the comparison turns it into a boolean. Note that returning n % 2 without the comparison would give 1 rather than False — a value that behaves as true in a condition, which would make the function report that 7 IS even. The comparison is what makes the return value mean what the name says.
Worked example
The natural way to use one, and the tempting redundant way.
if is_divisible(x, y):
print('x is divisible by y')
if is_divisible(x, y) == True:
print('x is divisible by y')| Form | What happens | Verdict |
|---|---|---|
| the first form | the call produces a boolean; if tests it | correct and direct |
| the second form | the boolean is compared with True | produces the same boolean again |
| the verdict | the extra comparison is unnecessary | and it reads worse |
Notice what an if statement needs.
Why: A boolean expression, from lesson 5b. A call to a boolean function is exactly that.
Notice what the comparison adds.
Why: True == True is True and False == True is False, so comparing a boolean with True gives back the same boolean. It is a round trip.
Prefer the direct form.
Why: It might be tempting to write the comparison, but the extra comparison is unnecessary — and the direct form reads as the sentence the name was chosen to make.
Figure (svg): A ladder showing a boolean function call being compared with True and reducing to the same boolean
Both work identically. The first form is preferred because the extra comparison computes nothing and obscures the question the function name was written to ask.
Verify: Read both forms aloud.
Why: If x is divisible by y versus if whether x is divisible by y equals true. The first is a sentence and the second is not, which is the readability argument in its most direct form — and readability is the whole reason the function was named that way.
Trap
A student writes a function returning True or False and calls it check_divisible or divisible_test.
Name it for what it does internally
Why: It does perform a check, so the name is not false.
Then the calling code reads if check divisible x y, which is not a question and gives no clue what a True answer would mean. Does True mean the check passed, or that it found a problem?
Name it so the call site reads as a yes-or-no question.
Start with is_, has_ or can_
Why: is_divisible, has_digit, can_afford. Each makes the call read as a question with an obvious answer.
Check by reading the if statement aloud
Why: If x is divisible by y is a sentence. If your name does not produce one, it is the wrong name.
This is the naming principle from lesson 4b applied to a specific case: the name is documentation Python does not check, and for a boolean function the thing most worth documenting is what True means.
Elimination
All four are legal. Only one makes the call site read as a question.
Eliminate the wrong options
A function returns True when a word contains a digit. Which name should it have?
Survives elimination: B
Why: has_digit makes the call site read as a sentence: if has_digit(word). It also makes clear what True means — that a digit is present — which is the thing a reader most needs to know and the thing the other three names leave open. The convention of starting with is_, has_ or can_ exists precisely because it forces this.
Faded example
The book's exercise. Return True when y lies between x and z inclusive.
Fill in the blanks
def is_between(x, y, z):
return x <= y and y <= z
Why: Both comparisons must hold, so the operator is and — and each is written out in full, as lesson 5a required. Note that Python's chained comparison would let you write x <= y <= z, which is shorter and reads exactly like the mathematics; both are correct, and the chained form is available because all the comparisons are about the same values.
Explain it to yourself
It is not wrong, only pointless. Say precisely why.
Discussion prompt
Explain why writing if is_divisible(x, y) == True: computes the same thing as if is_divisible(x, y):, using what you know about what the == operator produces.
Hint: What does True == True give?
Answer:
The call already produces a boolean. Comparing a boolean with True gives back the same boolean: True == True is True, and False == True is False.
So the comparison is a round trip that consumes a value and produces the identical value. The if statement then tests exactly what it would have tested anyway.
The reason to care is readability rather than speed. The name is_divisible was chosen so the call site reads as a question, and equals True turns the question back into a computation — undoing the abstraction the function existed to provide.
Section
Section 2
Concept
A recursive definition is similar to a circular definition, in the sense that the definition contains a reference to the thing being defined. A truly circular definition is not very useful — but a recursive one, with a base case, is.
# the mathematical definition:
# 0! = 1
# n! = n * (n-1)!
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result| Definition | What it says | The code |
|---|---|---|
| 0! = 1 | the base case in the definition | if n == 0: return 1 |
| n! = n * (n-1)! | the recursive case | return n * factorial(n-1) |
| the translation | one line of code per line of definition | almost mechanical |
The book contrasts this with a genuinely circular definition — vorpal: an adjective used to describe something that is vorpal — which tells you nothing. What makes the factorial definition useful rather than circular is the first line, which defines one case without reference to itself.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 55-56
Picture it
Two lines of mathematics become two branches of an if statement.
Figure (svg): Two columns showing the mathematical definition of factorial beside the Python function that implements it
That claim of the book's is worth taking literally: the translation really is line for line, and the hard work was done when the definition was written.
Worked example
The book builds it in three stages, which is incremental development from the previous lesson.
# step 1: decide the parameter
def factorial(n):
pass
# step 2: handle the base case
def factorial(n):
if n == 0:
return 1
# step 3: the recursive case
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)| Stage | What it adds | Where it comes from |
|---|---|---|
| step 1 | what should the parameter be? | an integer |
| step 2 | if the argument is 0, return 1 | the base case, straight from the definition |
| step 3 | otherwise multiply by the factorial of n-1 | the recursive case |
Decide the parameter first.
Why: The first step is to decide what the parameters should be. In this case it should be clear that factorial takes an integer.
Write the base case.
Why: If the argument happens to be 0, all we have to do is return 1. This comes straight from the first line of the definition.
Write the recursive case.
Why: Otherwise — and this is the interesting part — make a recursive call to find the factorial of n-1 and multiply it by n.
Figure (svg): The state of the program after each line of Worked example building factorial one step at a time, drawn as a ladder with one rung per traced line
Three stages, each taken directly from the definition. The parameter is decided first, then the base case, then the recursive case — which is the same order as the definition itself.
Verify: Check the function against a value you can compute by hand.
Why: 3! is 3 times 2 times 1, which is 6, and factorial(3) returns 6. Choosing a test case whose answer you know is lesson 6a's advice, and factorials are convenient because the small ones are all memorable.
Prediction
Four times the factorial of three.
Predict first
What is factorial(4)?
Correct: 24 — four times factorial(3), which is 6.
Why: The definition says n! is n times (n-1)!, so 4! is 4 times 3!, and 3! is 6. Using the previously computed answer rather than recomputing from scratch is the leap of faith in miniature: you do not need to re-trace factorial(3) if you already know it is 6. That is exactly the move the last section of this lesson asks you to make deliberately.
Worked example
The book's own trace. Follow the values back up as well as down.
>>> factorial(3)
6| Call | What it does | Result |
|---|---|---|
| factorial(3) | 3 is not 0, so compute factorial(2) first | waits |
| factorial(2) | 2 is not 0, so compute factorial(1) first | waits |
| factorial(1) | 1 is not 0, so compute factorial(0) first | waits |
| factorial(0) | 0 equals 0: return 1 with no further calls | returns 1 |
Follow the descent.
Why: Since 3 is not 0, we take the second branch and calculate the factorial of n-1 — and so on down to zero.
Reach the base case.
Why: Since 0 equals 0, we take the first branch and return 1 without making any more recursive calls.
Follow the ascent, which is new.
Why: The return value 1 is multiplied by n, which is 1, and returned. That 1 is multiplied by 2 and returned. That 2 is multiplied by 3, giving 6.
Figure (svg): A call diagram showing factorial calling itself down to zero and the return values one, one, two and six coming back up
6. The descent goes 3, 2, 1, 0 without computing anything; the multiplications all happen on the way back up, as each call receives its recursive call's return value.
Verify: Check that the multiplications happen in the order the trace says.
Why: The products are 11, then 21, then 3*2 — giving 1, 2 and 6. Multiplying in the other order would give the same answer here, since multiplication is commutative, but the trace shows which value each frame actually computes, and that is what figure 6.1 draws.
Trap
A student traces factorial(3) and expects the multiplication to happen as each call is made, so that by the time the base case is reached the answer is known.
Carry over the countdown model from lesson 5c
Why: There the printing did happen on the way down, so it is a reasonable generalisation.
Here nothing at all is computed on the way down. Each call reaches the recursive call and stops, waiting — and the answer only starts forming after the base case returns.
Ask which side of the recursive call the work is on, exactly as in lesson 5c.
In factorial the multiplication is AFTER the call
Why: n * factorial(n-1) cannot compute anything until the inner call returns, so all the arithmetic happens on the way back up.
In countdown the print was BEFORE the call
Why: Which is why its output appeared on the descent.
Same question, two answers, and it determines everything about how the function behaves. Figure 6.1 exists to make the upward flow of return values visible, because it is the part that is hard to see in the code.
Ranking
The calls go down; the products come back up.
Put in order
Why: Nothing is multiplied until the base case returns, because every multiplication needs the recursive call's value first. Then the products happen from the deepest frame outward: the frame with n equal to 1 multiplies first, then 2, then 3. This ordering is the whole content of figure 6.1, and it is invisible in the source code — which is why the diagram is worth drawing.
Translation
Each line of the mathematical definition becomes one part of the function.
Match the pairs
Why: The translation is close to mechanical, which is the book's point: if you can write a recursive definition of something, you can write a Python program to evaluate it. The real work is producing the definition, and in mathematics that work has usually been done for you — which is why factorial and fibonacci are the standard examples.
Counterexample
The book gives one. Say what distinguishes it from factorial.
Discussion prompt
Vorpal: an adjective used to describe something that is vorpal. Explain exactly what this definition lacks that the factorial definition has, and why that makes it useless rather than merely unhelpful.
Hint: Try to determine whether a particular thing is vorpal.
Answer:
It has no base case. There is no thing whose vorpalness is settled without reference to something else's vorpalness, so no chain of reasoning ever terminates.
Factorial has one: 0! = 1, stated outright. Every chain of self-references ends there, which is what converts the definition into a procedure.
So the difference is exactly the difference between a working recursion and an infinite one from lesson 5c. A recursive definition without a base case is not a weaker definition — it is not a definition at all.
Section
Section 3
Concept
The stack diagram for factorial looks like the one for countdown with one addition: the return values are shown being passed back up the stack. In each frame, the return value is the value of result, which is the product of n and recurse.
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result| Frame | Its local variables | What it returns |
|---|---|---|
| n = 3 | recurse = 2, result = 6 | returns 6 |
| n = 2 | recurse = 1, result = 2 | returns 2 |
| n = 1 | recurse = 1, result = 1 | returns 1 |
| n = 0 | neither variable exists | returns 1 |
In the last frame, the local variables recurse and result do not exist, because the branch that creates them does not run. That detail is worth noticing: the base-case frame is genuinely different from the others, and the diagram shows it.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 56-57 — figure 6.1
Picture it
The values flowing upward are what distinguish this from lesson 5c's diagram.
Figure (svg): A stack diagram for factorial of three showing four frames with n, recurse and result in each except the base case
Reading the diagram upward from the bottom shows the answer being built: 1, then 1, then 2, then 6.
Worked example
Every frame does the same tiny piece of work. Check it on the middle one.
# the frame where n = 2:
# recurse = factorial(1) -> 1
# result = n * recurse -> 2 * 1 = 2
# return result -> 2| Local variable | Where its value comes from | Value |
|---|---|---|
| recurse | whatever factorial(1) returned | 1 |
| result | n times recurse | 2 |
| return | hands result back to the frame above | 2 |
Identify what the frame receives.
Why: Its own n, which is 2, and the return value of its recursive call, which is 1.
Identify what it computes.
Why: One multiplication. In each frame the return value is the value of result, which is the product of n and recurse.
Identify what it hands back.
Why: That product, to the frame above it — which will multiply it by its own n in turn.
Figure (svg): The state of the program after each line of Worked example reading one frame at a time, drawn as a ladder with one rung per traced line
Two. The frame took the value from below, multiplied by its own n, and passed the result up. Every non-base frame does exactly this and nothing else.
Verify: Apply the same reading to the frame where n is 3.
Why: It receives 2 from below, multiplies by 3, and returns 6. That every frame follows an identical rule is the thing to notice — it is why the leap of faith works, and why you only ever need to check one frame.
Invariant
Step through factorial(3) and watch the return values form.
Step through it
How many multiplications happen in total, and how many happen before the base case is reached?
Three multiplications, and none of them before the base case returns. Every frame is waiting at its recursive call until the value arrives from below, which is why nothing is computed on the descent.
Worked example
It has fewer variables than the others. Work out why that is not an oversight.
def factorial(n):
if n == 0:
return 1
else:
recurse = factorial(n-1)
result = n * recurse
return result| Frame with n = 0 | Which branch runs | Which variables exist |
|---|---|---|
| n = 0 | takes the if branch | returns immediately |
| recurse | created only in the else branch | never created |
| result | created only in the else branch | never created |
Look at which branch the base case takes.
Why: The first one, which contains a single return statement and no assignments.
Conclude which variables exist.
Why: In the last frame the local variables recurse and result do not exist, because the branch that creates them does not run.
Notice what this says about local variables generally.
Why: A local variable comes into existence when its assignment RUNS, not when the function is called. A branch that does not run creates nothing.
Figure (svg): Two columns comparing a recursive frame holding three variables with the base case frame holding only n
The base-case frame contains only n, because the assignments to recurse and result are inside the else branch and that branch never runs for it.
Verify: Check the same claim on an ordinary if statement.
Why: A variable assigned only inside a false branch does not exist afterwards, and using it gives a NameError. The recursion has not introduced a new rule — it has made an existing one visible, because the diagram shows two frames of the same function with different variables in them.
Trap
A student sees the base case return 1 and expects that 1 to be the answer the original call produces.
Treat return as finish the whole thing
Why: It does end a function, so extending that to the whole recursion is a natural slip.
It returns to the frame directly above it — not to the top. That frame then does its multiplication and returns in turn, and the value changes at every level.
A return goes to the caller, and the caller is one frame up.
Follow the value one frame at a time
Why: The 1 goes to the frame with n equal to 1, becomes its recurse, is multiplied, and a different value continues upward.
Read figure 6.1 from the bottom up
Why: The numbers beside the arrows are 1, 1, 2, 6 — four different values, one per level.
This is why the diagram draws the return values explicitly. In the code they are invisible: n * recurse looks like one multiplication, and it happens once per frame with a different pair of numbers each time.
Prediction
Count the calls, including the base case and __main__.
Predict first
At the deepest point of factorial(4), how many frames are on the stack?
Correct: Six — __main__ plus factorial frames for n equal to 4, 3, 2, 1 and 0.
Why: The argument runs from 4 down to 0 inclusive, which is five calls, and none has returned yet. Adding __main__ gives six. This is the same counting as lesson 5c: the base case gets a frame like any other call, and __main__ is a frame too.
Fill the middle
The frame's n and its recurse are given.
Fill in the blanks
the frame where n = 3
recurse = 2
result = n * recurse = 6
returns 6
Why: In each frame the return value is the value of result, which is the product of n and recurse — here 3 times 2. Both blanks are the same number because the frame returns exactly what it computed; result is a temporary variable that exists to make the computation visible, and the concise version of the function returns the product directly without naming it.
Explain it
The commonest confusion about factorial, and figure 6.1 is the answer.
Discussion prompt
A classmate says they can see the calls going down but cannot see where the number 6 comes from. Explain, using the diagram, and say which single line of the function does the work.
Hint: Point at one line and say how many times it runs.
Answer:
Point at result = n * recurse. It runs once per frame, three times in total, with a different n and a different recurse each time.
Explain that nothing is multiplied on the way down: every frame stops at its recursive call and waits. The first number to exist is the 1 the base case returns.
Then walk it upward: that 1 is multiplied by 1, giving 1; that by 2, giving 2; that by 3, giving 6. The 6 was built by three multiplications in three different frames, which is why it is invisible in any single line of the code.
Section
Section 4
Concept
Following the flow of execution is one way to read programs, but it can quickly become overwhelming. An alternative is what the book calls the leap of faith: when you come to a function call, instead of following the flow of execution, you assume that the function works correctly and returns the right result.
leap of faith — Reading a function call by assuming the function works, rather than by following the flow of execution into it.
You are already practising this whenever you use built-in functions. When you call math.cos or math.exp you do not examine their bodies — you assume that they work because the people who wrote them were good programmers. The same is true when you call one of your own functions, once you have convinced yourself it is correct by examining the code and testing.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 57-57
Picture it
One follows the flow inward. The other stops at the interface.
Figure (svg): Two columns contrasting tracing the flow into a function with assuming the call works and reading only the current level
The leap of faith is not carelessness: it is exactly what you already do with library functions, and it is only justified once the function has been checked.
Worked example
One question replaces the whole trace. Ask it.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)| Move | What you do | Result |
|---|---|---|
| assume | factorial(n-1) returns the factorial of n-1 | the leap |
| ask | can I build the factorial of n from that? | the question |
| answer | yes: multiply by n | the recursive case is correct |
Take the leap.
Why: When you get to the recursive call, instead of following the flow of execution, assume that the recursive call works and returns the correct result.
Ask the one question.
Why: Assuming that I can find the factorial of n minus one, can I compute the factorial of n? It is clear that you can, by multiplying by n.
Check the base case separately.
Why: The leap does not cover the base case, because there is no recursive call there. 0! is 1, which the code returns directly.
Figure (svg): The state of the program after each line of Worked example reading factorial with the leap of faith, drawn as a ladder with one rung per traced line
The function is correct: the base case is right by inspection, and the recursive case is right because multiplying the factorial of n minus one by n gives the factorial of n. No tracing was needed.
Verify: Try the same reading on a version with a bug, to check the method catches it.
Why: If the recursive case returned n * factorial(n-2), the question becomes can I build n! from (n-2)! by multiplying by n — and the answer is no. The leap of faith is a genuine check rather than an assumption that everything is fine; it just checks one level instead of all of them.
Prediction
Be precise about what is assumed and what is checked.
Predict first
When reading a recursive function with the leap of faith, what do you assume?
Correct: That the recursive call returns the right result for its smaller input — and then you check whether this level builds the right answer from it.
Why: The assumption is narrow and the checking is real. You assume one thing — that the inner call is correct — and then ask a question that can be answered no: can I build this level's answer from it? Assuming the whole function is correct would be circular, and termination is a separate check that the leap does not perform.
Worked example
It is a method, not a licence. Work out what has to be true first.
# you may take the leap when:
# 1. the base case is correct by inspection
# 2. the recursive call is on a SMALLER input
# 3. this level's work is correct GIVEN the assumption| Condition | Why it is needed | How to check |
|---|---|---|
| condition 1 | the base case needs no assumption | check it directly |
| condition 2 | otherwise the recursion never terminates | lesson 5c's check |
| condition 3 | the actual question the leap asks | one level of reasoning |
Notice that the leap covers only one of the three.
Why: It answers the third question. The first two still have to be checked by hand, and they are the ones from lesson 5c.
Notice what happens if the second fails.
Why: The reasoning would be circular: assuming the call works and using it to prove the function works is only valid if the call is on something SMALLER, so that the chain terminates.
Notice the analogy the book draws.
Why: The same is true when you call one of your own functions. Once we have convinced ourselves that a function is correct — by examining the code and testing — we can use it without looking at the body again.
Figure (svg): A flow chart showing the three checks for a recursive function with the leap of faith as the third
Three conditions: a correct base case, a strictly smaller recursive call, and correct work at this level given the assumption. The leap of faith is the third; the first two are the termination checks you already know.
Verify: Ask what the leap would tell you about an infinite recursion.
Why: It would say the recursive case is correct, which it might well be — an infinite recursion can have a perfectly good recursive case and no reachable base case. That is why the leap is not a complete proof on its own, and why the book presents it alongside the base-case checks rather than instead of them.
Trap
A student assumes a helper function works, builds three more functions on top of it, and only then discovers the helper was wrong.
Read assume that the function works as do not check it
Why: The phrase does sound like permission.
Now four functions give wrong answers and the fault is in one of them, with no way to tell which from the outside.
Convince yourself the function is correct, THEN stop looking at it.
Examine the code and test it
Why: The book is explicit: once we have convinced ourselves that this function is correct — by examining the code and testing — we can use the function without looking at the body again.
Note that this is why library functions can be trusted
Why: You assume math.cos works because it has been examined and tested by other people, not because assuming is free.
The leap of faith is what you do AFTER verification, and it is what makes verification worth doing: a function you have checked once can be used a hundred times without being checked again.
Socratic
The book says it is a bit strange. Say exactly what is strange.
Discussion prompt
The book notes that it is a bit strange to assume the function works correctly when you have not finished writing it. Explain why that is not actually circular reasoning.
Hint: The assumption is about a different input.
Answer:
Because the assumption is about a SMALLER input, not the same one. Assuming factorial(n-1) is right in order to establish that factorial(n) is right is not assuming what you are proving.
Chained downward, the argument terminates at the base case, which is checked directly with no assumption at all — so the whole chain rests on something solid.
This is mathematical induction, and the reason it feels like a leap is that the justification is spread over the whole chain rather than being visible at any one step. It is genuinely valid, which is why the same reasoning proves theorems.
Analogy
Match each thing you trust to what you actually rely on.
Match the pairs
Why: The fourth row matters as much as the other three. The leap of faith is what you do with code you trust, and debugging is precisely the situation where you withdraw that trust from one function and look inside. Knowing when to take the leap and when not to is what makes it a technique rather than a habit.
Edge cases
The book introduces it for recursion and applies it more widely.
Discussion prompt
The leap of faith is presented for recursive calls. Does the same move apply to an ordinary call to another function you wrote? What changes, and what stays the same?
Hint: The book says explicitly that it does.
Answer:
It applies unchanged: once you have convinced yourself a function is correct, you use it without looking at the body again. That is the same move, and it is what makes a program of fifty functions readable at all.
What changes is the justification. For an ordinary call the trust comes from having tested that function; for a recursive call it also needs the argument to be smaller, so that the chain terminates.
So recursion needs one extra condition and is otherwise the same. That framing is more useful than treating recursion as a special case, because it means the reading skill you build here applies to every call you ever make.
Section
Section 5
Concept
A boolean function names a test; a recursive function returns a value; and the leap of faith is how you check the second without tracing it. Together they let you write a recursive function and be confident it is right before you run it.
def is_power_of_two(n):
if n == 1:
return True
if n % 2 != 0:
return False
return is_power_of_two(n // 2)| Case | Reasoning | Result |
|---|---|---|
| n == 1 | the base case: 1 is 2 to the power 0 | True |
| n odd and not 1 | an odd number above 1 cannot be a power of two | False |
| otherwise | halve it and ask again | the recursive call |
This function is boolean, recursive and fruitful at once — and it has two base cases rather than one, which is perfectly ordinary. What matters is that every path returns and that the recursive call is on a smaller input.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 54-57
Picture it
Not every recursion has exactly one base case.
Figure (svg): A flow chart showing two base cases returning True and False and a recursive case halving the input
The two base cases are the two ways the question can be settled without halving: it is already 1, or it is odd and therefore not a power of two.
Worked example
Three checks, none of which requires tracing.
# check 1: base cases
# n == 1 -> True. 1 is 2**0. Correct.
# n odd -> False. An odd number above 1 has an odd factor. Correct.
# check 2: smaller input
# n // 2 < n for every n above 1. Correct.
# check 3: the leap
# assuming is_power_of_two(n // 2) is right,
# is n a power of two exactly when n // 2 is? Yes, for even n.| Check | What it establishes | Note |
|---|---|---|
| check 1 | both base cases correct by inspection | no assumption needed |
| check 2 | halving strictly decreases | termination guaranteed |
| check 3 | this level's reasoning | the leap |
Check the base cases directly.
Why: They involve no recursion, so they can be verified by inspection alone. One is a power of two; an odd number greater than one is not.
Check that the input shrinks.
Why: Floor division by two strictly decreases any n above one, and the odd case has already been excluded, so the argument always reaches one.
Take the leap for the recursive case.
Why: Assume the call on n // 2 is right. For an even n, n is a power of two exactly when n // 2 is — so returning that call's answer is correct.
Figure (svg): The state of the program after each line of Worked example checking it with the leap of faith, drawn as a ladder with one rung per traced line
Correct, established by three checks and no tracing at all. The function was never run during this reasoning.
Verify: Run it on a few values to confirm the reasoning.
Why: 8 gives True, 12 gives False, 1 gives True. Running afterwards is still worth doing — the reasoning can contain a mistake — but it is confirming a conclusion rather than discovering one, which is a much better position to be in.
Sorting
Apply all three checks to each.
Sort into buckets
For a positive integer argument, sort each function.
Worked example
The leap of faith would pass this function. Find out what it misses.
def is_power_of_two(n):
if n % 2 != 0:
return False
return is_power_of_two(n // 2)| Input | What happens | Verdict |
|---|---|---|
| n = 8 | 8, 4, 2, then 1 | 1 is odd, so returns False — WRONG |
| the leap | would say the recursive case is fine | and it is |
| what is missing | the n == 1 base case | not something the leap checks |
Apply the leap and watch it pass.
Why: Assuming the call on n // 2 is right, an even n is a power of two exactly when n // 2 is. That reasoning is unchanged and still correct.
Trace an actual input.
Why: 8 halves to 4, to 2, to 1 — and 1 is odd, so the odd branch returns False. Eight is a power of two, so the answer is wrong.
Identify what went wrong.
Why: The n == 1 base case was removed, so the recursion runs one step too far and 1 falls into the odd case.
Figure (svg): A panel comparing the three checks against a version missing a base case, showing which check fails
It returns False for every power of two. The recursive case is correct and one base case is missing, which the leap of faith does not check.
Verify: Confirm which of the three checks would have caught it.
Why: The first: are the base cases correct? There is now only one, and it wrongly classifies 1. That is why the leap of faith is presented as one of three checks rather than as a complete method — it verifies the recursive step and nothing else.
Trap
A student writes a function named is_something that returns 1 or 0, or the value found rather than True or False.
Rely on the truthiness rule
Why: It works in a condition, and lesson 5a showed that nonzero counts as true.
Now a caller who writes result == True gets False even when the answer was yes, and a caller who prints the result sees 1 rather than True. The name promised a boolean and the function did not deliver one.
A function named like a question should return exactly True or False.
Return the comparison directly where you can
Why: return x % y == 0 produces a genuine bool rather than something that merely behaves like one.
Where the branches return literals, use True and False
Why: Not 1 and 0, and not the value that happened to be found.
This is the interface-as-contract idea from lesson 4b: the name is a promise about the return value, and returning something truthy-but-not-boolean breaks it in a way no error will report.
Prediction
Apply the two base cases in order.
def is_power_of_two(n):
if n == 1:
return True
if n % 2 != 0:
return False
return is_power_of_two(n // 2)| Call | Which branch | Result |
|---|---|---|
| n = 6 | not 1, and even | recurse on 3 |
| n = 3 | not 1, and odd | return False |
| back up | the False is returned unchanged | False |
Predict first
What does is_power_of_two(6) return?
Correct: False — six halves to three, which is odd and not one, so the odd base case returns False.
Why: Notice what the outer frame does with that False: it returns it unchanged, because the recursive call's value IS this level's answer. That is different from factorial, where each frame multiplied the value it received. A recursive function that passes its inner call's result straight back is a common and simpler shape.
Reverse engineer
The recursive case is right. One base case is missing.
Fill in the blanks
def is_power_of_two(n):
if n == 1:
return True
if n % 2 != 0:
return False
return is_power_of_two(n // 2)
Why: One is two to the power zero, so it is a power of two and the recursion must stop there. Without this case, halving continues until n reaches 1, which is odd, and the odd branch reports False for every power of two. Note that the order matters: this test has to come before the odd test, because 1 is itself odd.
Real world
It is how anybody reads a large program.
Discussion prompt
You open a program of five thousand lines and need to understand one function in the middle of it. Describe how the leap of faith makes that possible, and what would happen without it.
Hint: Count how many function bodies you would have to read.
Answer:
You read the one function, and for every call it makes you read the name and the docstring rather than the body. That is a page of reading rather than five thousand lines.
Without it you would have to follow the flow into every call, and into every call those make, until you had read most of the program to understand one function.
This is why lesson 4b cared so much about names and docstrings: the leap of faith is only available if the interface tells you what the function does. A badly named function forces the reader to look inside, which means it has destroyed the abstraction it was supposed to provide.
Comparison
Fill the blanks. The difference is which side of the recursive call the work is on.
Comparison matrix
| Question | countdown (lesson 5c) | factorial |
|---|---|---|
| Is it fruitful? | no — it prints | yes — it returns a number |
| Where is the work? | before the recursive call | after it: n * recurse |
| When does output or value appear? | on the way down | on the way back up |
| What does the base case do? | prints Blastoff | returns 1, starting the chain of products |
The second row decides the third. Asking which side of the recursive call the work is on is the first question to ask about any recursive function.
Pattern
Three checks, and the third is the leap of faith.
Steps 1 and 2 are lesson 5c's termination checks and steps 3 and 4 are new. The leap of faith checks only the recursive step, which is why it cannot replace the other two.
Python documentation — More Control Flow Tools More Control Flow Tools
Check
The comparison is already a boolean.
def is_positive(n):
return n > 0| Part | What it produces | Note |
|---|---|---|
| n > 0 | a relational operator | produces a bool |
| return | hands back that bool | True or False |
| no if needed | the comparison is already the answer | concise |
Check your understanding
Why does this function need no if statement?
Answer: B
Why: The expression n > 0 already evaluates to exactly True or False, which is what the function is supposed to return. An if statement that returned True when it was True and False when it was False would be returning it unchanged, at greater length — which is the redundancy the book points out about the long form of is_divisible.
Check
The multiplication is after the recursive call.
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n-1)| Stage | What happens | Note |
|---|---|---|
| descent | each call reaches the recursive call and waits | no multiplication yet |
| base case | returns 1 | the first value |
| ascent | each frame multiplies and returns | the answer is built |
Check your understanding
When does the first multiplication happen in factorial(3)?
Answer: B
Why: Every frame reaches n * factorial(n-1) and cannot multiply until the inner call returns a value, so all three frames are waiting when the base case runs. The base case returns 1, and only then does the frame with n equal to 1 perform the first multiplication. This is why figure 6.1 draws the return values flowing upward.
Check
Be precise about what the leap does and does not check.
Check your understanding
Which of these does the leap of faith NOT establish?
Answer: B
Why: Termination is a separate question, answered by the base-case checks from lesson 5c: does a base case exist, and is it reachable from every input? A function can have a perfectly correct recursive step and never terminate, which is why the leap of faith is one of three checks rather than a complete method.
Real world
The leap of faith is how anybody uses anything they did not build.
Discussion prompt
Name something you use daily whose internals you could not explain. What are you relying on instead of understanding, and what would have to happen for you to stop trusting it and look inside?
Hint: The second half is the interesting one.
Answer:
Almost everything: a car, a lift, a payment system. What you rely on is the interface — it does this when you do that — plus evidence that other people have checked it.
You stop trusting it and look inside when it misbehaves. That is exactly the debugging move: the leap of faith is suspended for the one component under suspicion, and maintained for everything else.
Which is why the leap is a technique rather than laziness. Trusting everything is naive and trusting nothing is impossible, and the skill is in knowing which single thing to stop trusting when something goes wrong.
Commit first
Answer, then rate your confidence. This is about where the answer is built.
Predict first
In factorial(3), how many multiplications happen BEFORE the base case returns?
Correct: None — every frame waits at its recursive call, so no multiplication can happen until a value comes back from the base case.
Why: The expression is n * factorial(n-1), and the multiplication cannot be performed until the call on its right has produced a value. So all three frames are suspended at that point when the base case runs, and the three products happen one at a time on the way back up. If you expected the work to happen on the descent, you were carrying over the countdown model from lesson 5c, where the print came BEFORE the recursive call — and asking which side of the call the work is on is the question that distinguishes them.
Explain it
The leap of faith sounds like a licence to skip work. It is not.
Discussion prompt
A classmate says the leap of faith just means assuming your code works, which cannot be a real technique. Explain what is actually assumed, what is actually checked, and why the reasoning is not circular.
Hint: The assumption is about a different input from the one you are proving.
Answer:
Say: you assume the recursive call is right for a SMALLER input, and then you check — genuinely check — whether this level builds the right answer from it. That check can fail, which is what makes it a technique.
It is not circular because the assumption is about a different, smaller input, and the chain of assumptions terminates at a base case that is verified directly with no assumption at all.
Then give them the comparison that makes it obvious: they already do this with math.sqrt. Nobody reads its body, and nobody calls that assuming your own code works — it is trusting a checked interface, which is what the leap of faith is.
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: Boolean functions are the quickest to settle and pay off immediately in readability. Translating a definition is nearly mechanical once you trust it, and the difficulty is almost always in finding the definition rather than coding it. Figure 6.1 is worth drawing by hand at least once, because the upward flow of return values is invisible in the source. And the leap of faith is the one that keeps growing — it is how you will read every large program you ever meet, and the next lesson needs it immediately, because fibonacci makes your head explode if you try to trace it.
Connect it up
One page, from memory. The diagram is the exercise.
Draw it
Draw the full stack diagram for factorial(3), with every frame, its n, its recurse and its result — and draw arrows for the return values with the numbers on them. Mark which frame is missing two variables and say why. Then, beside it, write the three checks for a recursive function, and mark which of the three the leap of faith performs and which two it does not.
Recap
Three pages, and a way of reading recursion that does not involve tracing it.
| If you remember one thing | It is this |
|---|---|
| From boolean functions | The comparison is already a boolean. Return it. |
| From naming | is_, has_, can_ — so the if statement reads as a sentence. |
| From factorial | The work is after the recursive call, so it all happens on the way back up. |
| From figure 6.1 | The base case's frame has fewer variables, because the other branch never ran. |
| From the leap of faith | It checks the recursive step and nothing else. Termination is a separate question. |
The next lesson finishes the chapter with fibonacci, which is unreadable by tracing and easy with the leap of faith, and then with what happens when a recursive function is given an argument its base case cannot handle.
Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 54-57 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.