1.2.39 · D4Introduction to Programming (Python)

Exercises — Recursion depth limit — stack overflow

2,512 words11 min readBack to topic

Before we begin, a tiny bit of vocabulary we will lean on — all built in the parent, restated in plain words so line one makes sense:


Level 1 — Recognition

L1.1 — Spot the missing finish line

Which of these functions will hit RecursionError no matter what you pass in?

def A(n):
    if n == 0: return 0
    return A(n - 1)
 
def B(n):
    return B(n + 1)
 
def C(n):
    if n <= 0: return 0
    return C(n)
Recall Solution
  • A — has a base case n==0 AND each call moves toward it (n-1). Safe for non-negative n within the depth limit. (Note: a negative n would overrun 0 and never stop — the domain assumption matters.)
  • B — no base case at all; n grows forever. Always overflows.
  • C — has a base case, but C(n) calls C(n) — it never moves toward the base. For any n>0 it always overflows.

Answer: B and C always overflow. A does not (for non-negative n with depth ≤ limit).

L1.2 — Read the depth

boom(0) from the parent calls boom(1), then boom(2), ... With the default limit of , which call number is the first to raise RecursionError, if boom(0) was the first frame?

Recall Solution

boom(0) is frame 1, boom(1) is frame 2, ..., boom(k) is frame . The limit of allows frames through to exist. Frame is boom(999) — that call is allowed. The next call, boom(1000), would be frame , which exceeds , so boom(1000) is the call that raises RecursionError. Key off-by-one: the deepest call that succeeds is boom(999) (frame ); the first call that fails is boom(1000) (frame ).


Level 2 — Application

L2.1 — Frames needed

For depth_sum(n) (base n==0, step n-1, ), how many frames are stacked at once, at the deepest point, for depth_sum(4)? Write the frames in the order they are pushed.

Recall Solution

Push order (each waits for the next): depth_sum(4), depth_sum(3), depth_sum(2), depth_sum(1), depth_sum(0). That is frames stacked simultaneously. Only when depth_sum(0) returns 0 do frames start popping (LIFO — last pushed, first popped).

L2.2 — Will it fit?

CPython's default limit is . Assume frames already exist (module + REPL bookkeeping) before you call. What is the largest n for which depth_sum(n) still finishes without RecursionError?

Recall Solution

Available depth for your recursion frames. depth_sum(n) needs frames. Require . Largest safe value: .

L2.3 — Set the limit

You must run depth_sum(2000). Using the total-frame rule with pre-existing frames, write the smallest safe sys.setrecursionlimit value if .

Recall Solution

Frames needed by recursion: . Total frames on the stack at the deepest point: . So you need the limit to be at least . sys.setrecursionlimit(2031) is the minimum; in practice you'd pad it (e.g. 3000) for safety.


Level 3 — Analysis

L3.1 — Two-branch recursion

def fib(n):          # domain: n >= 0
    if n < 2: return n
    return fib(n - 1) + fib(n - 2)

fib calls itself twice. Does this make the maximum recursion depth twice as big? What is the deepest the stack ever gets for fib(10)?

What the figure below shows: it draws the call tree for a small case, fib(4), as a set of rounded boxes. fib(4) at the top branches into fib(3) (left) and fib(2) (right); each of those branches again, down to the base cases fib(1) and fib(0). The coral (pink) path and green boxes trace the single deepest chain — the one that always takes the n-1 (left) branch: fib(4) → fib(3) → fib(2) → fib(1). The grey boxes/edges are the other calls. A butter-yellow note reminds you that the left branch fully finishes and pops its frames before the right branch starts, so those calls never pile up together. The point: the tree is wide (many calls) but only the deepest chain decides the stack depth. The same rule applied to fib(10) gives a maximum depth of .

Figure — Recursion depth limit — stack overflow
Recall Solution

No. Depth is the longest single chain of paused calls, not the total number of calls. Look at the figure: the tree is wide (many calls) but the deepest path only ever goes down the n-1 branch: fib(10) → fib(9) → fib(8) → ... → fib(1). That path pushes frames for , so the frames are for = frames maximum stacked at once. Total calls is huge ( calls for fib(10)), but they don't all live at the same time — the two branches run one after another, so frames pop between them.

L3.2 — Depth vs call count

For fib(n) (), give a formula for the maximum depth (frames at once) and explain why it is NOT the total call count.

Recall Solution

Max depth follows the always-n-1 path: fib(n) → fib(n-1) → ... → fib(1), giving depth for (and depth for ). Total call count grows roughly like , where is the golden ratio — a fixed constant a bit larger than ; raising it to the power makes the count balloon exponentially as grows. That is a time/repetition cost, paid sequentially. Depth is a space cost, and only the frames currently paused count. Since the left subtree fully finishes (popping its frames) before the right subtree starts, they never coexist. So depth = (grows linearly), while call count (grows exponentially).


Level 4 — Synthesis

L4.1 — Convert to iteration

Rewrite depth_sum iteratively so it works for n = 5_000_000 without touching sys.setrecursionlimit. What is depth_sum_iter(100)?

Recall Solution
def depth_sum_iter(n):
    total = 0
    while n > 0:
        total += n
        n -= 1
    return total

The loop reuses one frame the whole time, so depth stays regardless of n. .

L4.2 — Safe deep call with recovery

Write safe_depth_sum(n) that returns the sum if it fits under the current limit, or None if it would overflow — without crashing. Confirm safe_depth_sum(3) returns 6.

Recall Solution
def depth_sum(n):
    if n == 0: return 0
    return n + depth_sum(n - 1)
 
def safe_depth_sum(n):
    try:
        return depth_sum(n)
    except RecursionError:
        return None

RecursionError is an ordinary exception (see Exceptions and try-except in Python), so try/except catches it — this is exactly why Python uses a soft, catchable limit instead of a segfault. .


Level 5 — Mastery

L5.1 — Budget the whole thing

You have a genuinely deep-but-finite recursion needing depth . Measurement shows pre-existing frames. Each frame uses about bytes of stack, and your OS gives the thread of stack — that is bytes (a MiB, mebibyte, is exactly bytes; a decimal MB would be bytes — we use the binary MiB here). (a) What is the minimum setrecursionlimit value? (b) How many bytes will the stack actually use at the deepest point? (c) Does it fit?

Recall Solution

(a) Total frames alive at the deepest point . Minimum limit . (b) Stack bytes used bytes ( MiB). (c) Available bytes. Since , yes, it fits — with bytes to spare ( MiB). This is the disciplined way to raise the limit: compute the byte budget first (see Memory model — stack vs heap), don't just "crank it to a million."

L5.2 — Why can't Python auto-fix deep tail recursion?

depth_sum ends with return n + depth_sum(n-1). Is this tail recursion? Could a language with tail-call optimization run it at depth ? What must you do in Python?

Recall Solution

It is not tail recursion: after depth_sum(n-1) returns, there is still work to do — the addition n + (...). The recursive call is not the last thing that happens, so its frame must stay alive to receive the result and add n. (See Tail recursion and why Python lacks tail-call optimization.) Even a true tail-recursive version (accumulator-style, return helper(n-1, acc+n)) would still overflow in Python, because CPython deliberately keeps every frame (for clean tracebacks) — it has no tail-call optimization. So in Python the fix is the same either way: convert to a loop, giving constant depth . depth_sum_iter(100) = 5050 from L4.1 confirms the loop matches the recursion.

L5.3 — The full cause-chain

In one clean sentence per link, reconstruct the mnemonic "No Base → Frames Race → Stack's out of Space" using the exact vocabulary from this page.

Recall Solution
  • No Base: a missing OR unreachable base case (including a step that moves away from the base, e.g. a negative input to depth_sum) means no recursive call ever returns.
  • Frames Race: because no call returns, no stack frame is ever popped, so the recursion depth climbs by with every call.
  • Stack's out of Space: depth crosses the recursion limit ( total frames), and CPython raises a catchable RecursionError before the real, fixed-size stack memory runs out and the OS segfaults.

Connections