Understanding Stack Frames
advanced20 minLearning objectives
- Explain how the call stack stores return addresses, parameters and local variables during function calls
Learn
AQA 4.1.1.15 — Stack frames
Retrieval: the Local Variables lesson (Year 12, Sequence 3) established that a variable declared inside a function only exists while that function is running, and can't be accessed from outside it. This lesson explains the actual mechanism that makes that true.
Key vocabulary
- Stack frame — a block of memory created for one function call, holding its parameters, local variables, and the return address.
- Return address — the exact point in the calling code execution should resume at once the called function finishes.
- Call stack — the stack (Year 12, Sequence 4) of stack frames for every function call currently in progress, LIFO by nature.
- Push / pop — a stack frame is pushed onto the call stack when its function is called, and popped off when that function returns.
Understand — why local variables disappear when a function ends
Every time a function is called, Python creates a fresh stack frame specifically for that call, holding that call's own parameters and local variables — completely separate from any other call's frame, even a call to the exact same function. When the function returns, its entire stack frame is popped and discarded, which is the actual mechanism behind "a local variable stops existing once its function ends": there's no lingering copy anywhere, because the memory holding it was part of a frame that no longer exists.
See it — three nested calls
def double(n):
result = n * 2
return result
def add_one(value):
incremented = value + 1
return incremented
def process(start):
doubled = double(start)
return add_one(doubled)
answer = process(5)
Trace it — the call stack, step by step
| Step | Action | Call stack (top to bottom) |
|---|---|---|
| 1 | process(5) called | process [start=5] |
| 2 | inside process, double(5) called | double [n=5] / process [start=5] |
| 3 | double returns 10; its frame is popped | process [start=5, doubled=10] |
| 4 | inside process, add_one(10) called | add_one [value=10] / process [start=5, doubled=10] |
| 5 | add_one returns 11; its frame is popped | process [start=5, doubled=10] |
| 6 | process returns 11; its frame is popped | (empty) |
At its deepest point (step 4), the call stack held two frames at once — but double's frame (step 2) and add_one's frame (step 4) were never simultaneously on the stack together, even though both were called from inside process. Each existed only for the duration of its own call, exactly LIFO.
Debug it — diagnose, explain, fix, test, justify
A student writes this, expecting it to print 15:
def calculate_total(price):
tax = price * 0.2
total = price + tax
return total
def show_receipt(price):
calculate_total(price)
print(f"Total: {total}")
show_receipt(12.5)
Running it raises NameError: name 'total' is not defined.
- Diagnose: which stack frame does the variable
totalactually belong to? - Explain: why can
show_receipt'sprintline not see it, even thoughcalculate_totalwas called directly above? - Fix: rewrite the two functions so
show_receiptcan correctly print the total. - Test: confirm your fixed version prints
Total: 15.0. - Justify: explain, in terms of stack frames being pushed and popped, exactly why the original code could never have worked as written.
(total belongs entirely to calculate_total's own stack frame, which is popped and discarded the moment calculate_total returns — show_receipt's frame never had access to it and still doesn't after the call ends. The fix is to have calculate_total return the value, and have show_receipt capture it: total = calculate_total(price), then print that.)
Common mistake
Assuming a variable set inside one function call is somehow still available to a different function afterwards, "because it was just calculated." Each stack frame is entirely private to its own call — the only way information leaves a function is via its return value (or a genuinely shared/global variable, deliberately declared as such).
Why this matters for recursion
Every recursive call pushes another stack frame, each with its own copy of the function's local variables and parameters — this is precisely why recursive functions can go wrong with a "maximum recursion depth exceeded" error: too many stack frames stack up before any of them return. You'll use this understanding directly in the next lesson.
Check your understanding
If a function calls itself 1,000 times before its base case is reached, how many stack frames exist on the call stack at that deepest point, immediately before any of them start returning? (1,000 — one frame per call, all still on the stack simultaneously since none has returned yet.)
Challenge
Trace the call stack for process(3) from the "See it" example above by hand, on paper, writing out the exact stack contents after every push and pop — then check your trace against the table above.
Looking ahead: the next lesson (Introduction to Recursion) puts this exact mechanism to direct use — a recursive function is really just a function that keeps pushing new stack frames for itself, each waiting on the one below it to finish.