Recursion & the Call Stack
Recursion and the call stack describe how a function calls itself without losing its place. Each call creates a stack frame, the newest frame runs first, and a return value travels back to the caller waiting below it. That stored frame is also why deep recursion can crash before the math gets hard.
The physical model is a stack of sticky notes. You write fact(4) on the first note, then it says, "I need fact(3) before I can finish." So you put a new note on top. Only the top note gets your attention. The lower notes are not gone; they are just waiting with unfinished work. In the animation, each sticky note is one stack frame, and a "note waits" label means that frame has unfinished local work.
Why recursion needs the call stack
A recursive function has two jobs. First, it needs a base case: a condition that can return without making another recursive call. Second, it needs a recursive case: a call on a smaller input, followed by whatever work uses that returned answer.
For factorial, the base case is n <= 1, which returns 1. The recursive case for fact(n) asks for fact(n - 1), stores that answer as child, then returns n * child.
That word "stores" matters. When fact(4) calls fact(3), the fact(4) frame does not vanish. It stays on the call stack with its local n = 4 and an unfinished slot, child = ?. When fact(3) eventually returns 6, that value lands in the waiting slot, and only then can fact(4) compute 4 * 6.
This is last-in, first-out control flow. The last call pushed is the first one that can return. The practical lesson is already visible here: every waiting frame costs stack memory.
Recursion & the Call Stack Step by Step
The default run computes fact(4).
- The program starts at
answer = fact(n), withn = 4. The stack is empty. fact(4)is called, so a new frame is pushed. That frame has local staten = 4.- The frame checks
4 <= 1. The answer is false, so it cannot return yet. fact(4)pauses atchild = fact(3). Itschildslot is waiting.fact(3)is pushed on top. Nowfact(3)runs, whilefact(4)waits below it.fact(3)checks3 <= 1, gets false, and pauses atchild = fact(2).fact(2)is pushed, checks2 <= 1, gets false, and pauses atchild = fact(1).fact(1)is pushed. Its guard1 <= 1is true, so the base case returns1.- That
1is delivered to the caller,fact(2), fillingchild = 1. fact(2)can now finish: it returns2 * 1 = 2.- That
2lands infact(3)aschild = 2, sofact(3)returns3 * 2 = 6. - That
6lands infact(4)aschild = 6, sofact(4)returns4 * 6 = 24. - The root return value becomes
answer = 24, and the stack is empty again.
The important visual signal is the direction change. Calls push frames upward; returns move values downward into the caller that was waiting. If you can explain why fact(2) finishes before fact(4), you have the call-stack model.
After the normal run, switch the code variant to missingBase. Remove the base case and watch why recursion must stop: every top frame asks for one more frame, and none of them reach a return.
Time and Space Complexity
| Variant | Time complexity | Stack space | What happens |
|---|---|---|---|
fact(n) with a base case | O(n) | O(n) | For n >= 1, one frame per value from n down to 1, then one return per frame. |
fact_without_base(n) | Does not terminate | Grows until a recursion limit | Every frame calls another frame; no frame reaches a return. |
Under this lesson's unit-cost model, the normal factorial program does constant work in each frame: one guard, maybe one recursive call, and at most one multiplication on the way back. With n frames, that is O(n) time and O(n) stack space. That stack-space line is the practical bite: if n can be huge, deep recursion can crash before the math is hard, which is when you rewrite it as a loop, use an explicit stack, or change the recursion limit only on purpose.
The missing-base variant is not "slow factorial." It is not factorial at all. It has no stopping rule, so the only thing that halts it is the runtime's recursion limit. Python's default limit is around 1000 frames, not six; this lesson uses a tiny visible cap so you can see the failure before the screen turns into wallpaper.
Edge Cases
n = 1hits the base case immediately. One frame is pushed, returns1, and pops.- The maximum normal input here is
n = 6, not because factorial stops there, but because six frames are the largest stack that fits cleanly on the teaching canvas. - In the missing-base variant, negative values do not save you.
fact_without_base(-2)still callsfact_without_base(-3), then-4, then keeps going. - A return value always goes to exactly one place: the caller's waiting
childslot, or the globalanswerslot for the root call.
Common Mistakes
- Thinking recursion creates copies of the whole program. It creates a new frame for the same function, with its own local variables.
- Forgetting that callers are suspended.
fact(4)cannot multiply untilfact(3)has returned. - Treating the base case as optional. Without it, the stack grows until the runtime stops the program.
- Mixing up return order with call order. Calls go
4, 3, 2, 1; returns come back1, 2, 6, 24. - Reusing one shared
childvariable in your mental model. Each frame has its ownchild;fact(2).childandfact(4).childare different slots.
A Note on Simplification
This visualization shows a teaching stack frame: function name, argument, local child, and return value. Real Python frames also carry bytecode positions, globals, closure references, exception state, and interpreter bookkeeping. Those details matter to runtime engineers, but they would hide the first idea: recursion works because each call has its own frame, and the top frame is the only one currently running.
Keep exploring
A few lessons that share the same technique or lead into the next idea.
Found a mistake or something unclear? Leave feedback.