Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 27

Tracing Recursive Calls with the Call Stack

Recommended resources

Hello! Welcome back to our study of recursion.

In our last lesson, you successfully wrote your first recursive function, factorial. We used print() statements to get a hint of what was happening "under the hood"—a series of function calls going "down" until a base case was hit, and then a series of returns coming back "up."

Today, we will pull back the curtain and explore the mechanism that makes this all possible: the call stack. Understanding the call stack is the single most important step to mastering recursion. It's the "rule book" the computer follows to keep track of all the nested calls, their local variables, and where to return to.

Your goal for this lesson is to trace recursive calls using the concept of the call stack.

What is a Stack?

Before we can understand the call stack, we first need to understand what a stack is in general. It's one of the simplest data structures in computer science.

Chapter 1 - What Is Recursion?

The following text from the book 'Invent with Python' provides a great introduction to the stack data structure using helpful analogies.

Please read the section titled 'What Are Stacks?'. It starts with 'Earlier I mentioned the clichéd wisecrack...' and ends just before the next section, 'What Is the Call Stack?'. Focus on the LIFO (Last-In, First-Out) principle and the terms 'pushing' and 'popping'.

To summarize, a stack is a LIFO (Last-In, First-Out) data structure. Think of it like a stack of plates:

  • You push a new plate onto the top of the stack.
  • You pop a plate off the top to use it.
    You can only interact with the topmost item. The last plate you added is the first one you take off.

How Programs Use Stacks: The Call Stack

Your computer uses a stack internally to manage function calls. This is known as the call stack. Every time a function is called, the computer creates a package of information called a stack frame and pushes it onto the call stack.

This frame contains important details for that specific function call, including:

  • The function's local variables (including its parameters).
  • The "return address"—where in the code to resume after the function finishes.

Let's watch a video that illustrates this process with a brilliant analogy.

Recursion in Programming - Full Course

This segment from a freeCodeCamp.org course explains the call stack using a work-interruption analogy, which makes the concept very intuitive. It then shows how this applies to a chain of simple function calls.

Watch from 10:48 to 14:15. Pay attention to how each 'interruption' (function call) adds a new task to the to-do list (the stack) and how tasks are completed in the reverse order they were added.

As the video showed, when a() calls b(), and b() calls c(), the stack looks like this:

Stack
c() frame
b() frame
a() frame
(bottom)

c() must finish and return first (it's popped off the stack). Then b() can finish, and finally a().

A crucial point is that each stack frame holds its own set of local variables. This is why two different function calls can have a local variable named spam without interfering with each other—they exist in separate frames on the call stack.


The Call Stack in Recursion

So, what does this have to do with recursion? A recursive call is still just a function call. The only difference is that the function is calling itself. Each time it does, a new stack frame with its own local variables is pushed onto the call stack.

Call Stack for a Recursive Function
This diagram shows what the call stack might look like during a recursive process. The `main()` function call is at the bottom. Each recursive call to `reduceByOne` adds a new frame to the top of thestack. The stack grows until a base case is reached, at which point it starts shrinking as functions return.

This also explains a common error in recursion: the stack overflow. If your recursive function never hits its base case, it will keep calling itself, pushing more and more frames onto the stack until the computer runs out of memory for the stack. To prevent this, Python stops the program and raises a RecursionError.


Tracing Factorial on the Call Stack

Let's apply this to our factorial function from the last lesson. We'll trace factorial(4). The following video provides a brilliant, step-by-step visualization of this process, building a diagram of the call stack as it grows and shrinks.

Tracing a recursive function

This video manually traces the execution of factorial(4), carefully drawing out each stack frame and showing how the return values are passed back up the stack. This is a powerful technique for you to use when analyzing recursive algorithms.

Watch from the beginning to 09:24. Follow along as the presenter traces the process: The 'Winding' Phase (00:00 - 07:04): Watch how the calls to fact(4), fact(3), fact(2), and fact(1) push new frames onto the stack. Each frame has its own value for n. The 'Unwinding' Phase (07:04 - 09:24): See what happens when the base case (fact(1)) is hit. It returns 1, its frame is popped, and the value is passed to the waiting fact(2) frame, which can now complete its calculation.

The process you just watched has two distinct phases:

  1. Winding: The function calls itself repeatedly, pushing new frames onto the stack until the base case is reached. No calculations are completed during this phase; they are all left pending.
    • factorial(4) is called. It needs factorial(3).
    • factorial(3) is called. It needs factorial(2).
    • factorial(2) is called. It needs factorial(1).
  2. Unwinding: Once the base case returns a value, the stack frames are popped one by one. Each frame uses the returned value from the call above it to complete its own calculation and return its result to the frame below.
    • factorial(1) returns 1.
    • factorial(2) receives 1, calculates 2 * 1, and returns 2.
    • factorial(3) receives 2, calculates 3 * 2, and returns 6.
    • factorial(4) receives 6, calculates 4 * 6, and returns 24.

This "winding" and "unwinding" is the fundamental execution pattern of most recursive functions.


Tracing a Non-Mathematical Example: String Reversal

The call stack works the same way for any type of data. Let's look at a function that recursively reverses a string.

Here's the Python code. See if you can apply the "recursive leap of faith":

  • Base Case: Reversing an empty string gives you an empty string.
  • Recursive Step: To reverse "hello", we can trust that our function can reverse "ello" to get "olle". Then we just need to stick the "h" on the end.
def reverse_string(s):



    # Base Case
    if len(s) == 0:
        return ""



    # Recursive Step
    else:



        # Recursively reverse the rest of the string (s[1:])
        reversed_substring = reverse_string(s[1:])



        # Add the first character (s[0]) to the end
        return reversed_substring + s[0]

print(reverse_string("hello")) # Output: olleh

Now, let's watch an animation that traces reverse_string("hello") on the call stack.

Recursion in Programming - Full Course

This animation from the same freeCodeCamp.org course provides a crystal-clear trace of the reverse_string function. It shows how the characters are held on the stack and then concatenated during the unwinding phase.

Watch the segment from 20:38 to 22:51. Notice how each stack frame is waiting to perform its concatenation (+ s[0]) until the recursive call above it returns a value.


Your Turn: Tracing countDownAndUp

Let's solidify your understanding with a final example. This function is interesting because it has code that runs after the recursive call.

def count_down_and_up(number):
    print(number) # Code before the recursive call

    if number == 0:



        # Base Case
        print("Reached the base case.")
        return
    else:



        # Recursive Step
        count_down_and_up(number - 1)
    



    # This code runs AFTER the recursive call above has completely finished!
    print(f"{number} returning")

count_down_and_up(3)

Before you look at the answer, take a piece of paper and try to trace the execution of count_down_and_up(3). Use what you know about the call stack. What do you predict the output will be?

...

Here is the actual output:

3
2
1
0
Reached the base case.
1 returning
2 returning
3 returning

Let's trace why this happens:

  1. count_down_and_up(3) is called. It prints 3 and then calls count_down_and_up(2). It now waits.
  2. count_down_and_up(2) is called. It prints 2 and then calls count_down_and_up(1). It waits.
  3. count_down_and_up(1) is called. It prints 1 and then calls count_down_and_up(0). It waits.
  4. count_down_and_up(0) is called. It prints 0, then prints "Reached the base case." and returns. Its frame is popped.
  5. Execution returns to the count_down_and_up(1) frame. It has finished its recursive call, so it now executes the code after the call: print("1 returning"). It then returns. Its frame is popped.
  6. Execution returns to the count_down_and_up(2) frame. It prints 2 returning and returns. Its frame is popped.
  7. Execution returns to the count_down_and_up(3) frame. It prints 3 returning and returns. The program is finished.

This example perfectly demonstrates that when a function call returns, execution picks up exactly where it left off in the calling function.


Conclusion

Fantastic work today! You've tackled the most critical concept for truly understanding recursion. By visualizing the call stack, you can demystify the "magic" and see the logical, step-by-step process the computer follows.

Key Takeaways:

  • The call stack is a LIFO (Last-In, First-Out) data structure that programs use to manage function calls.
  • Each function call pushes a stack frame onto the stack, containing its own local variables and a return address.
  • In recursion, each self-call adds a new frame, causing the stack to wind.
  • When the base case is hit, the stack begins to unwind as frames are popped, values are returned, and pending calculations are completed.
  • A stack overflow (RecursionError) occurs when the stack grows too large, usually because a base case is never reached.

Preview of the Next Lesson:
Now that we can implement and trace recursive functions, we need to think about their performance. In the next lesson, we will analyze the exponential time complexity of a naive recursive Fibonacci function. You'll see that while recursion can be elegant, a naive implementation can be incredibly inefficient, which will set the stage for learning about algorithm analysis and optimization techniques.