6b Boolean Functions, More Recursion, and the Leap of Faith

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

What this lesson covers

The lesson, slide by slide

1. Lesson 6b Boolean Functions, More Recursion, and the Leap of Faith

Title

Python · Chapter 6 — Fruitful functions

§6.4-6.6, pp. 54-57

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.4-6.6, pp. 54-57 — the pages these objectives are drawn from

3. Before we start: do you read the body of math.sqrt?

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.

4. The one idea behind this lesson: a name can stand in for a computation

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

Both are correct. The right-hand one says what it means.

Think Python, 2nd edition — Allen B. Downey §6.4-6.6, pp. 54-54

5. Boolean functions: hiding a test behind a question

Section

Section 1

6. Returning True or False

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
CallWhat happensReturn value
is_divisible(6, 4)6 % 4 is 2, not 0False
is_divisible(6, 3)6 % 3 is 0True
the namereads as a yes/no questionis 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

7. Picture it: the same function, written twice

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 result of the == operator is already a boolean, so it can be returned 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.

8. Worked example: returning the comparison directly

Worked example

The if-else is redundant. Work out exactly why.

def is_divisible(x, y):
    return x % y == 0
LayerWhat it producesNote
x % yarithmetica number
... == 0a comparisonTrue or False
returnhands back that booleanno 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

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

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.

9. Predict: what does this return?

Prediction

The comparison is already a boolean.

def is_even(n):
    return n % 2 == 0

print(is_even(7))
StepWhat happensValue
7 % 2the remainder1
1 == 0the comparisonFalse
returnhands back FalseFalse

Predict first

What does this print?

  • True
  • False
  • 1
  • 0

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.

10. Worked example: using a boolean function in a condition

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')
FormWhat happensVerdict
the first formthe call produces a boolean; if tests itcorrect and direct
the second formthe boolean is compared with Trueproduces the same boolean again
the verdictthe extra comparison is unnecessaryand 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

The comparison is a round trip: it produces exactly what it was given.

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.

11. Trap: naming a boolean function like an action

Trap

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

The fix

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.

12. Eliminate: which name is right for a boolean function?

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?

  • A. check_digit
  • B. has_digit
  • C. digit
  • D. find_digit

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.

13. Complete it: write is_between

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.

14. Explain it yourself: why is == True unnecessary?

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.

15. Recursive definitions, and translating one into code

Section

Section 2

16. A definition that refers to the thing being defined

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
DefinitionWhat it saysThe code
0! = 1the base case in the definitionif n == 0: return 1
n! = n * (n-1)!the recursive casereturn n * factorial(n-1)
the translationone line of code per line of definitionalmost 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

17. Picture it: the definition and the code, side by side

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

If you can write a recursive definition of something, you can write a Python program to evaluate 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.

18. Worked example: building factorial one step at a time

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)
StageWhat it addsWhere it comes from
step 1what should the parameter be?an integer
step 2if the argument is 0, return 1the base case, straight from the definition
step 3otherwise multiply by the factorial of n-1the 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

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

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.

19. Predict: what does factorial(4) return?

Prediction

Four times the factorial of three.

Predict first

What is factorial(4)?

  • 12
  • 24
  • 10
  • 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.

20. Worked example: tracing factorial(3)

Worked example

The book's own trace. Follow the values back up as well as down.

>>> factorial(3)
6
CallWhat it doesResult
factorial(3)3 is not 0, so compute factorial(2) firstwaits
factorial(2)2 is not 0, so compute factorial(1) firstwaits
factorial(1)1 is not 0, so compute factorial(0) firstwaits
factorial(0)0 equals 0: return 1 with no further callsreturns 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.

21. Trap: expecting the work to happen on the way down

Trap

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

The fix

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.

22. Rank: the order in which the multiplications happen

Ranking

The calls go down; the products come back up.

Put in order

  1. the base case returns 1
  2. 1 * 1, giving 1
  3. 2 * 1, giving 2
  4. 3 * 2, giving 6

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.

23. Translate: definition to code

Translation

Each line of the mathematical definition becomes one part of the function.

Match the pairs

  • a. 0! = 1
  • b. n! = n * (n-1)!
  • c. the symbol ! applied to an integer
  • d. the reference to (n-1)! inside the definition
  • r1. if n == 0: return 1
  • r2. return n * factorial(n-1)
  • r3. def factorial(n):
  • r4. the recursive call

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.

24. Find the counterexample: when is a self-reference useless?

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.

25. Stack diagrams with return values

Section

Section 3

26. Figure 6.1, and what is new about it

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
FrameIts local variablesWhat it returns
n = 3recurse = 2, result = 6returns 6
n = 2recurse = 1, result = 2returns 2
n = 1recurse = 1, result = 1returns 1
n = 0neither variable existsreturns 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

27. Picture it: four frames, each with its own recurse and result

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

The book's figure 6.1. Each result is that frame's n times its recurse.

Reading the diagram upward from the bottom shows the answer being built: 1, then 1, then 2, then 6.

28. Worked example: reading one frame at a time

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 variableWhere its value comes fromValue
recursewhatever factorial(1) returned1
resultn times recurse2
returnhands result back to the frame above2

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

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

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.

29. Watch the stack: values coming back up

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?

  1. All four frames are created on the way down and none of them has computed anything yet.
  2. The base case is the first frame to produce a value. It returns 1 without multiplying.
  3. That 1 becomes recurse in the frame above, which multiplies by its n of 1 and returns 1.
  4. That 1 becomes recurse in the next frame, which multiplies by 2 and returns 2.
  5. That 2 becomes recurse in the outermost frame, which multiplies by 3 and returns 6 to __main__.

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.

30. Worked example: why the base-case frame is different

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 = 0Which branch runsWhich variables exist
n = 0takes the if branchreturns immediately
recursecreated only in the else branchnever created
resultcreated only in the else branchnever 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

Same function, same code, different variables — because a different branch ran.

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.

31. Trap: thinking the return value goes back to __main__

Trap

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

The fix

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.

32. Predict: how many frames for factorial(4)?

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?

  • Four
  • Five
  • Six
  • Three

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.

33. Fill the middle: what does this frame return?

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.

34. Explain it: where does the answer get built?

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.

35. The leap of faith

Section

Section 4

36. Reading a recursion without tracing it

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

37. Picture it: two ways to read a call

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

Both are legitimate. The second scales and the first does not.

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.

38. Worked example: reading factorial with the leap of faith

Worked example

One question replaces the whole trace. Ask it.

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
MoveWhat you doResult
assumefactorial(n-1) returns the factorial of n-1the leap
askcan I build the factorial of n from that?the question
answeryes: multiply by nthe 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 whole run at once: each drop is one line of the program.

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.

39. Predict: what does the leap of faith ask you to assume?

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?

  • That the whole function is correct, so no checking is needed
  • That the recursive call returns the right result for its smaller input
  • That the base case will eventually be reached
  • That the function terminates

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.

40. Worked example: when the leap is justified

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
ConditionWhy it is neededHow to check
condition 1the base case needs no assumptioncheck it directly
condition 2otherwise the recursion never terminateslesson 5c's check
condition 3the actual question the leap asksone 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.

41. Trap: taking the leap on an untested function

Trap

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

The fix

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.

42. Think it through: why is it a *leap*?

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.

43. Analogical pivot: the leap you already take

Analogy

Match each thing you trust to what you actually rely on.

Match the pairs

  • a. calling math.cos
  • b. calling your own tested is_divisible
  • c. calling factorial(n-1) inside factorial
  • d. reading the body of a function you are debugging
  • r1. trusting that its authors were good programmers
  • r2. trusting your own examination and testing
  • r3. trusting the induction: it terminates at a checked base case
  • r4. not taking the leap, because this is the thing under suspicion

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.

44. Push the boundary: does the leap work for non-recursive calls?

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.

45. Putting it together: writing a recursive function you can trust

Section

Section 5

46. All three ideas, applied at once

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)
CaseReasoningResult
n == 1the base case: 1 is 2 to the power 0True
n odd and not 1an odd number above 1 cannot be a power of twoFalse
otherwisehalve it and ask againthe 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

47. Picture it: two base cases and one recursive case

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.

48. Worked example: checking it with the leap of faith

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.
CheckWhat it establishesNote
check 1both base cases correct by inspectionno assumption needed
check 2halving strictly decreasestermination guaranteed
check 3this level's reasoningthe 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

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

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.

49. Sort: does this recursive function terminate and return correctly?

Sorting

Apply all three checks to each.

Sort into buckets

For a positive integer argument, sort each function.

terminates and is correct
if n == 0: return 1; else: return n * f(n-1); if n <= 0: return 1; else: return n * f(n-1); if n == 1: return True; if n % 2: return False; return f(n // 2)
fails one of the three checks
return n * f(n-1) -- no base case; if n == 0: return 1; else: return n * f(n-2); if n % 2: return False; return f(n // 2)
ok
Each has a base case that is correct and reachable from every positive input, a strictly smaller recursive call, and a correct step given the leap.
no
One has no base case at all; one subtracts two from an odd number and steps over an exact-equality base case; and one is missing the base case for 1, so every power of two is misclassified.

50. Worked example: a version with a missing base case

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)
InputWhat happensVerdict
n = 88, 4, 2, then 11 is odd, so returns False — WRONG
the leapwould say the recursive case is fineand it is
what is missingthe n == 1 base casenot 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.

51. Trap: a boolean function that returns something other than a boolean

Trap

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

The fix

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.

52. Predict: what does this return for 6?

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)
CallWhich branchResult
n = 6not 1, and evenrecurse on 3
n = 3not 1, and oddreturn False
back upthe False is returned unchangedFalse

Predict first

What does is_power_of_two(6) return?

  • True
  • False
  • 3
  • None

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.

53. Reverse engineer: supply the missing base case

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.

54. Where the leap of faith pays off

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.

55. Compare: two recursive functions you have now seen

Comparison

Fill the blanks. The difference is which side of the recursive call the work is on.

Comparison matrix

Questioncountdown (lesson 5c)factorial
Is it fruitful?no — it printsyes — it returns a number
Where is the work?before the recursive callafter it: n * recurse
When does output or value appear?on the way downon the way back up
What does the base case do?prints Blastoffreturns 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.

56. The procedure: checking a recursive function without running it

Pattern

Three checks, and the third is the leap of faith.

  1. Find every base case and verify it directly — no assumption is needed, because no recursion is involved.
  2. Check that every recursive call is on a strictly smaller input, and that every legal input reaches a base case.
  3. Take the leap: assume the recursive call returns the correct answer for its input.
  4. Ask whether this level builds the correct answer from that assumption. If yes, the recursive case is right.
  5. Confirm that every path through the function reaches a return statement, as in the previous lesson.
  6. Then run it on a value whose answer you know, to check the reasoning rather than to discover the answer.

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

57. Check yourself 1 of 3: boolean functions

Check

The comparison is already a boolean.

def is_positive(n):
    return n > 0
PartWhat it producesNote
n > 0a relational operatorproduces a bool
returnhands back that boolTrue or False
no if neededthe comparison is already the answerconcise

Check your understanding

Why does this function need no if statement?

  • A. Because Python adds one automatically
  • B. Because the result of a relational operator is already a boolean (correct)
  • C. Because the function is too short for an if statement
  • D. Because return converts its argument to a boolean

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.

Why A tempts people
Python adds nothing. The comparison produces the value directly, so there is nothing for an if statement to contribute.
Why C tempts people
Length is not the criterion. A long function returning a comparison directly would be equally right, and a short one with a redundant if would be equally redundant.
Why D tempts people
return converts nothing. It hands back whatever value the expression produced, which here happens already to be a bool.

58. Check yourself 2 of 3: where the work happens

Check

The multiplication is after the recursive call.

def factorial(n):
    if n == 0:
        return 1
    else:
        return n * factorial(n-1)
StageWhat happensNote
descenteach call reaches the recursive call and waitsno multiplication yet
base casereturns 1the first value
ascenteach frame multiplies and returnsthe answer is built

Check your understanding

When does the first multiplication happen in factorial(3)?

  • A. Immediately, in the first call
  • B. After the base case returns (correct)
  • C. All three at once, at the end
  • D. Never — factorial only compares

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.

Why A tempts people
The first call cannot multiply, because the value it needs to multiply by does not exist until the recursion has reached the bottom and come back.
Why C tempts people
They happen one at a time, in three different frames, as each recursive call returns. The diagram shows three separate products.
Why D tempts people
It certainly multiplies — that is the entire recursive case. What it does not do is multiply on the way down.

59. Check yourself 3 of 3: the leap of faith

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?

  • A. That this level computes the right answer given a correct recursive call
  • B. That the recursion terminates (correct)
  • C. That the recursive case is correctly written
  • D. That the function can be read without tracing

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.

Why A tempts people
This is exactly what the leap establishes: assuming the call works, does multiplying by n give the right answer?
Why C tempts people
This is the same thing as A, and it is what the leap is for.
Why D tempts people
This is the practical benefit of the leap. Reading one level rather than all of them is why it exists.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

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?

  • Three
  • Two
  • One
  • None

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.

62. Explain it to someone else

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.

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?

  • Boolean functions: returning the comparison directly, and naming them as questions
  • Turning a recursive definition into a recursive function
  • Reading a stack diagram with return values flowing back up
  • The leap of faith: what it assumes and what it checks

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Three pages, and a way of reading recursion that does not involve tracing it.

If you remember one thingIt is this
From boolean functionsThe comparison is already a boolean. Return it.
From namingis_, has_, can_ — so the if statement reads as a sentence.
From factorialThe work is after the recursive call, so it all happens on the way back up.
From figure 6.1The base case's frame has fewer variables, because the other branch never ran.
From the leap of faithIt 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

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.4-6.6, pp. 54-57
  2. Python documentation — More Control Flow Tools
  3. Python documentation — Built-in Types

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

Book on Wyzant · Text (657) 465-8108