MODULE 05 / 12crash course
~/roadmap/04-recursion
Beginner

Recursion: Thinking in Smaller Selves

Recursion explained from first principles — base case, recursive case, the call stack, and why your function sometimes blows up. The mental model every programmer needs before touching trees, graphs, or dynamic programming.

9 min read2026-01-15Ironclad Academy

The moment it clicks

Here is something you have probably done without thinking: looked up a word in the dictionary, found the definition used another word you did not know, looked that up, found it used a third word, and so on until you finally hit something obvious. Each step depended on a smaller, more basic definition. You stopped when the words became self-evident. That is recursion. Base case: word you already know. Recursive case: look up the next word.

The computer version is identical, just more formal. A function sees a problem, decides "I can almost solve this — I just need someone to solve a slightly smaller version first," calls itself with that smaller version, and waits. When the smaller version returns with an answer, the original function finishes its own work and returns to whoever called it.

Two questions to always ask:

  1. What is the smallest input I can answer without thinking? (Base case.)
  2. How do I reduce any other input into something smaller, and what do I do with the result? (Recursive case.)

Answer those two questions and you have the function.

Factorial: the textbook example, explained properly

factorial(n) is the product of every integer from 1 to n. So factorial(4) is 4 × 3 × 2 × 1 = 24.

The recursive insight: factorial(4) is just 4 × factorial(3). And factorial(3) is 3 × factorial(2). You keep peeling one layer off until you hit factorial(1), which is 1 by definition. You do not need recursion to tell you that — it is the base case.

def factorial(n):
    # Base case: we know the answer directly
    if n <= 1:
        return 1
    # Recursive case: peel one layer, delegate the rest
    return n * factorial(n - 1)

That is four lines of code that would take three paragraphs of arithmetic to unroll manually. But what does the computer actually do when you call factorial(4)?

The call stack: the thing nobody explains

Every time a function is called, your program pushes a stack frame onto the call stack. That frame holds the function's local variables, its parameters, and a pointer to where execution should return when the function finishes. When the function returns, the frame is popped off.

For a normal function, frames push and pop in quick alternation — one at a time, mostly invisible. For a recursive function, a whole chain of frames push before any of them pop. The stack literally grows with each call:

flowchart TB
    A["factorial(4) — waiting for factorial(3)"]
    B["factorial(3) — waiting for factorial(2)"]
    C["factorial(2) — waiting for factorial(1)"]
    D["factorial(1) — returns 1"]

    D --> C
    C --> B
    B --> A

    style A fill:#7c5cff,color:#fff
    style B fill:#6a4fd4,color:#fff
    style C fill:#5843ab,color:#fff
    style D fill:#00ff9d,color:#0a0a0f

factorial(4) calls factorial(3) and freezes. factorial(3) calls factorial(2) and freezes. factorial(2) calls factorial(1) — which hits the base case and returns 1. Now the stack unwinds: factorial(2) wakes up, multiplies 2 × 1 and returns 2. factorial(3) wakes up, multiplies 3 × 2 and returns 6. factorial(4) wakes up, multiplies 4 × 6 and returns 24. Done.

Hit play on the visualizer above and watch that play out frame by frame. The value you care about — 24 — does not exist anywhere until the very last unwind step. Everything before it is anticipation.

The visualizer: step through it yourself

{ "type": "recursion", "fn": "factorial", "n": 4, "title": "factorial(4) call stack" }

Step through one frame at a time. Notice:

  • Going down (growing): each call has its own copy of n. They are independent — factorial(3)'s n is not the same variable as factorial(4)'s n.
  • Coming back up (unwinding): the actual multiplication happens on the return trip, not on the way in. A lot of confusion about recursion comes from thinking the work happens going down.

This is why writing a recursive function feels different from debugging one. Writing it, you think top-down: "factorial(4) is 4 times whatever factorial(3) returns." Debugging it, you trace bottom-up: "factorial(1) returned 1, so factorial(2) returned 2, so..."

Stack overflow: when the stack runs out

The call stack is not infinite. On most systems it can hold something in the range of 1,000 to 10,000 frames, depending on the language and how much each frame holds. Call factorial(100000) and you will get a RecursionError (Python) or a StackOverflowError (Java/JS). The program does not crash because the logic is wrong — it crashes because memory ran out.

Python even bakes this in explicitly. The default recursion limit is 1,000:

import sys
sys.getrecursionlimit()  # 1000

# You can raise it, but this is usually a sign your algorithm needs rethinking
sys.setrecursionlimit(10000)

The three causes of stack overflow, in order of how often you will hit them:

  1. Missing base case. The function never stops calling itself. Always check that your base case is actually reachable.
  2. Wrong base case. The base case exists, but some inputs skip past it (n starts negative, an edge case is unhandled).
  3. Input genuinely too deep. The algorithm is correct but the recursion depth exceeds what the stack can hold. For inputs this large, you either need iteration or an explicit stack data structure.

Fibonacci: where recursion teaches you its dark side

Fibonacci is the next "hello world" of recursion. fib(n) is the sum of the two before it: fib(n) = fib(n-1) + fib(n-2), with fib(0) = 0 and fib(1) = 1.

def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

This is correct. It is also catastrophically slow once n gets past about 40. Why?

Because every call fans out into two calls. fib(5) calls fib(4) and fib(3). fib(4) calls fib(3) and fib(2). Now fib(3) is being computed twice. And each of those duplicates their own sub-calls:

flowchart TB
    A["fib(5)"]
    B["fib(4)"]
    C["fib(3)"]
    D["fib(3)"]
    E["fib(2)"]
    F["fib(2)"]
    G["fib(1)"]
    H["fib(2)"]
    I["fib(1)"]
    J["fib(1)"]
    K["fib(0)"]

    A --> B
    A --> C
    B --> D
    B --> E
    D --> F
    D --> G
    E --> H
    E --> I
    C --> J
    C --> K

    style A fill:#7c5cff,color:#fff
    style C fill:#ff4d8d,color:#fff
    style D fill:#ff4d8d,color:#fff

fib(3) appears twice. fib(2) appears three times. By fib(50), you are recomputing the same tiny subproblems over a quadrillion times. The call tree roughly doubles at every level — that is O(2^n) time.

The fix is to remember results the first time you compute them and reuse them instead of recomputing. That technique is called memoization, and it is the entry point to dynamic programming. The naive recursive solution and the memoized one are identical in structure — one just caches.

from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n - 1) + fib(n - 2)

Same code, one decorator. Now each subproblem is computed exactly once — O(n) time, O(n) space. The repeated-work problem evaporates. This is the pattern you will see again and again: recursion expresses the structure beautifully, and memoization makes it efficient.

The shape of recursive problems

Recursion is not the right tool everywhere. For summing a list or iterating through an array, a for loop is clearer and cheaper. But there is a class of problems where the recursive structure is so natural that forcing it into a loop makes the code ugly and brittle.

Those problems share a shape: the data or the task is self-similar. Some examples:

ProblemWhy recursion fits
Tree traversalA tree is a node plus subtrees, which are also trees
File system walkA directory contains files and other directories
Parsing nested JSONAn object contains values, which may themselves be objects
Merge sort / quicksortSort the halves, then combine
Generating all subsetsInclude element, recurse; exclude element, recurse
Maze / path findingTry a direction, recurse; backtrack if stuck

Notice the last one. Backtracking — the technique for exhaustively searching decision trees — is almost always written recursively. The "try something, recurse, undo it" pattern is extremely awkward to express with explicit loops.

A complete worked example: power function

power(base, exp) — raise base to the integer exponent exp. The naive approach multiplies exp times in a loop (O(n)). Recursion lets us do better.

The key observation: base^8 = base^4 × base^4. We only need to compute base^4 once. So:

def power(base, exp):
    # Base cases
    if exp == 0:
        return 1
    if exp == 1:
        return base

    # For even exponents: base^exp = (base^(exp/2))^2
    half = power(base, exp // 2)
    if exp % 2 == 0:
        return half * half
    else:
        # Odd: base^5 = base * base^4
        return base * half * half

This is O(log n) — each call halves the exponent, so there are only about log₂(exp) levels of recursion. That is the same asymptotic win you get from binary search, and for the same reason: cutting the problem in half at each step is exponentially better than peeling one element off at a time. Check out Big-O notation if the O(log n) reasoning is not yet crisp for you.

What you should feel, not just know

Recursion confuses people because it asks you to trust something you have not verified yet. When you write return n * factorial(n - 1), you are trusting that factorial(n - 1) will return the right answer before you have written the proof that it does. That feels circular.

The way out is the leap of faith: assume the smaller call works correctly. Do not trace the whole stack in your head. Just ask: "If factorial(n-1) gives me the right answer for (n-1), what do I need to do with it to get the answer for n?" Multiply by n. Done. The machine handles the rest.

This is the mental shift. Once you can take the leap of faith — assume the smaller problem is solved, just describe what you do with the result — recursive functions become as easy to write as iterative ones.

What comes next

You now have the foundation. Two things build directly on it:

Backtracking uses recursion to explore decision trees — you try an option, recurse into the consequences, and if you hit a dead end you undo (backtrack) and try the next option. Generating all permutations, solving Sudoku, N-Queens — all backtracking.

Dynamic programming takes the exponential-work problem you just saw in Fibonacci and solves it systematically. If a recursive problem has overlapping subproblems (same inputs being recomputed), memoization or a bottom-up table cuts the time dramatically. DP is recursion plus memory.

Both of those techniques are recursive at their core. Everything you just learned applies directly.

// FAQ

Frequently asked questions

What is recursion in simple terms?

Recursion is when a function calls itself to solve a smaller version of the same problem. Every recursive solution has two parts: a base case (the simplest input you can answer directly, which stops the chain) and a recursive case (where the function calls itself on a smaller input and uses that result to build its own answer). Without the base case, the function runs forever — or until the program crashes.

What is the call stack and why does it matter for recursion?

The call stack is a region of memory where your program tracks which function is running and what local variables it holds. Every function call pushes a new frame onto the stack; every return pops one off. Recursive functions push many frames in a row before any of them return. If the recursion goes deep enough — typically tens of thousands of calls — the stack runs out of space and you get a stack overflow error.

What is a base case in recursion and why is it required?

The base case is the condition that stops the recursion — an input small enough that you can answer it directly without calling yourself again. Without a base case, the function calls itself forever. With the wrong base case (one that is never actually reached), you get the same infinite loop. Every recursive function must have at least one base case that is guaranteed to be hit.

Why is naive recursive Fibonacci so slow?

Naive recursive Fibonacci recomputes the same subproblems over and over. To compute fib(5), it calls fib(4) and fib(3). fib(4) then calls fib(3) and fib(2) — fib(3) gets computed twice. The tree of calls doubles roughly at every level, giving O(2^n) time. For fib(50), that is over a quadrillion operations. Memoization or bottom-up dynamic programming cuts this to O(n) by storing each answer the first time it is computed.

When should I use recursion instead of a loop?

Recursion shines when the problem has a naturally recursive structure — trees, graphs, divide-and-conquer, backtracking. If you are traversing a file system, parsing nested JSON, generating all subsets, or searching a maze, recursion often produces clearer code than an explicit loop. For flat, sequential work like summing a list or iterating over an array, a loop is simpler and avoids the call-stack overhead. The rule of thumb: if the problem decomposes into same-shaped sub-problems, reach for recursion.