Recursion and the Call Stack
Recursion is a way to solve a problem by defining it in terms of a smaller version of itself. In Python, this means a function calls itself.
The syllabus expects you to understand recursion, trace recursive and non-recursive programs, and understand the use of stacks in recursive programming.
The Two Required Parts
A recursive function needs:
- a base case: the case that stops recursion;
- a recursive case: the case that calls the same function with a smaller or simpler input.
Without a reachable base case, the function keeps calling itself until Python raises an error.
Countdown Example
def countdown(n):
if n == 0:
print("Done")
else:
print(n)
countdown(n - 1)For countdown(3), the output is:
3
2
1
DoneThe base case is n == 0. The recursive case prints n and calls countdown(n - 1).
Order Matters
If the recursive call happens before the print(), the output changes.
Caption: Recursive calls move toward the base case; returned values then resolve the waiting expressions in reverse order.
def countup(n):
if n == 0:
print("Done")
else:
countup(n - 1)
print(n)For countup(3), the output is:
Done
1
2
3The calls go down to the base case first. Then the waiting calls continue in reverse order.
This is the main beginner trap in recursion: the line after the recursive call does not run immediately. It waits until the smaller call has finished.
Tracing Calls and Returns
A complete recursive trace has two phases:
- Call phase: each call records its local values and waits for the smaller call.
- Return phase: the base case finishes, then waiting calls resume in last-in, first-out order.
For written traces, record both phases. A table that lists only the downward calls may miss output or calculations that occur after the recursive call.
Recursive Return Values
Some recursive functions return values.
def factorial(n):
# Assumes n is a positive integer.
if n == 1:
return 1
return n * factorial(n - 1)Trace for factorial(4):
| Call | What it needs | Return value |
|---|---|---|
factorial(4) | 4 * factorial(3) | 24 |
factorial(3) | 3 * factorial(2) | 6 |
factorial(2) | 2 * factorial(1) | 2 |
factorial(1) | base case | 1 |
The final answer is built as the calls return:
factorial(1) returns 1
factorial(2) returns 2 * 1 = 2
factorial(3) returns 3 * 2 = 6
factorial(4) returns 4 * 6 = 24Stack Frames and Return Addresses
A stack frame stores the information needed for one active function call, including its local variables and where execution should continue when the called function returns.
The Call Stack
When a function is called, Python keeps information about that call in a stack frame. The frame includes local values and the point where execution should resume after the call returns.
Caption: Each recursive call pushes a stack frame containing its local state and waiting work; frames pop in reverse order as values return.
The stack follows last-in, first-out order. When a recursive call is made, a new frame is pushed. When that call returns, its frame is popped:
- the most recent call is completed first;
- earlier calls wait underneath it;
- each return removes one frame from the stack.
Recursive Versus Iterative Thinking
Iterative factorial:
def factorial_iterative(n):
# Assumes n is a positive integer.
result = 1
while n > 1:
result = result * n
n = n - 1
return resultRecursive factorial:
def factorial_recursive(n):
# Assumes n is a positive integer.
if n == 1:
return 1
return n * factorial_recursive(n - 1)Both can compute the same value. The iterative version updates variables inside a loop. The recursive version relies on smaller function calls and the call stack.
Choosing a Valid Base Case
The base case must match the allowed input domain. The earlier factorial version assumes a positive integer. A version that accepts zero should use :
def factorial(n):
# Assumes n is a non-negative integer.
if n == 0:
return 1
return n * factorial(n - 1)For invalid negative input, the recursive argument moves farther from the base case. In practical code, validate the input before recursion.
Benefits and Drawbacks
Recursion can be elegant when a problem naturally has smaller subproblems, such as tree traversal or processing a nested structure. It can also be easier to reason about once the base case and recursive case are clear.
Drawbacks:
- each call uses stack memory;
- too many calls may exceed the recursion limit;
- a missing or wrong base case causes non-termination;
- some recursive solutions are less efficient than iterative ones.
Common Bugs
- No base case.
- Base case exists but is never reached.
- Recursive call does not make the problem smaller.
- Returning a value in the base case but forgetting to return in the recursive case.
- Printing during recursion when the question asks for a returned value.
- Confusing call order with return order.
Practice Trace
Trace:
def mystery(n):
if n == 1:
return 1
return mystery(n - 1) + nFor mystery(4):
mystery(1) = 1
mystery(2) = 1 + 2 = 3
mystery(3) = 3 + 3 = 6
mystery(4) = 6 + 4 = 10This computes the sum from 1 to n.
Check Your Understanding
Try these before looking back at the explanations:
- What two parts must every correct recursive solution have?
- Why does
countup(3)print1only afterDone? - What happens to a stack frame when a function returns?
- In
factorial(4), which call reaches the base case first?
Answers:
- A base case and a recursive case.
- The recursive calls reach the base case before the waiting
print(n)statements resume. - It is removed from the call stack.
factorial(1).