Recursion and the Call Stack

Recursion solves a problem through calls that eventually return to a reachable base case. Direct recursion means a function calls itself; indirect recursion means A calls B and B eventually calls A. The examples here use direct recursion.

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: a call whose state makes measurable progress toward a reachable base case. Numerically smaller input is common but not universal.

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)

Contract: n is a non-negative integer. Reject negative or non-integer input before calling this teaching version; subtracting 1 from an invalid value may never reach exactly 0.

For countdown(3), the output is:

3
2
1
Done

The 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: For countup(3), calls descend 3 -> 2 -> 1 -> 0. The base case prints Done; then each suspended call resumes after its child and prints 1, 2, 3 while the stack unwinds.

def countup(n):
    if n == 0:
        print("Done")
    else:
        countup(n - 1)
        print(n)

This function has the same non-negative-integer contract as countdown.

For countup(3), the output is:

Done
1
2
3

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

  1. Call phase: each call records its local values and waits for the smaller call.
  2. 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):

CallWhat it needsReturn value
factorial(4)4 * factorial(3)24
factorial(3)3 * factorial(2)6
factorial(2)2 * factorial(1)2
factorial(1)base case1

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 = 24

Stack 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: Keep maximum depth separate from unwinding. At maximum depth, four frames exist but only factorial(1) has produced 1; then one frame at a time resumes and pops, producing 2, 6, and 24 in LIFO order.

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 result

Recursive 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

Boundary-test recursive functions at the base case (0 here), first recursive case (1), a typical valid value, a negative value, and a non-integer. For countdown(-1), the trace -1, -2, -3, ... shows the first failure is the input contract/progress measure, not the eventual RecursionError. Validate before recursion and rerun the accepted and rejected cases.

  • 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) + n

Contract: n is a positive integer. The recursive step reduces n by 1, so valid calls reach base case 1; zero, negative, or non-integer inputs must be rejected before calling this teaching version.

For mystery(4):

mystery(1) = 1
mystery(2) = 1 + 2 = 3
mystery(3) = 3 + 3 = 6
mystery(4) = 6 + 4 = 10

For valid positive integer n, this computes the sum from 1 to n.

Check Your Understanding

Try these before looking back at the explanations:

  1. What two parts must every correct recursive solution have?
  2. Why does countup(3) print 1 only after Done?
  3. What happens to a stack frame when a function returns?
  4. In factorial(4), which call reaches the base case first?

Answers:

  1. A base case and a recursive case.
  2. The recursive calls reach the base case before the waiting print(n) statements resume.
  3. It is removed from the call stack.
  4. factorial(1).