skip to content

Recursion Versus Looping

CPython eliminates no tail calls, so deep recursion raises RecursionError near the default limit; the answers are sys.setrecursionlimit, a bigger thread stack, or an explicit stack and a loop.

part ofPythonoverview, primer and where to startread it →
on this pageshow

questions

4

What does sys.getrecursionlimit() return on a stock CPython, and what does exceeding it raise?

level: juniorimportance: must knowfreq 72%

answer

  1. A guard rail, not the hardware limit
  2. Roughly one thousand of something
  3. Counted per thread, set per interpreter
  4. sys.getrecursionlimit reports the ceiling
  5. The error is a RuntimeError subclass

basics

~10 s

A stock CPython reports 1000 from sys.getrecursionlimit(). Crossing it raises RecursionError, a subclass of RuntimeError. The number counts active Python frames on the current thread, not bytes of stack.

solid answer

~40 s

CPython refuses to recurse until the machine's stack runs out. It keeps a per-thread count of active Python frames and compares it against an interpreter-wide ceiling that `sys.getrecursionlimit()` reports; on a stock build that ceiling is 1000 and has been for many releases, 3.14 included. The next call that would cross it raises `RecursionError`, which subclasses `RuntimeError` (it was split out in Python 3.5), so a broad `except RuntimeError` or `except Exception` will swallow it. The budget is shared with every frame already live, so a real function bottoms out somewhat below 1000 once you count the module frame, decorator wrappers and generator expressions. In practice, hitting the limit almost always means a missing base case or a cycle in data you assumed was acyclic — diagnose before you reach for a bigger number.

code

python · 12 lines
python
import sys

print(sys.getrecursionlimit())                    # 1000 on a stock CPython 3.14
print(issubclass(RecursionError, RuntimeError))   # True

def depth(n=1):
    return depth(n + 1)

try:
    depth()
except RecursionError as exc:
    print(type(exc).__name__, exc)

go deeper

for a junior

Be ready to state the number and the exception name without hedging: 1000 by default, sys.getrecursionlimit() reports it, RecursionError when you cross it. Say that your first suspicion is a missing base case, not a limit that is too low.

for a middle

Explain the mechanics: a per-thread frame counter against an interpreter-wide ceiling, RecursionError as a RuntimeError subclass, and the fact that frames already on the stack spend part of the budget. Mention that decorators and generator expressions cost frames.

for a senior

An interviewer expects you to read the collapsed traceback and name the cause — runaway recursion, mutual recursion, or a cycle in supposedly acyclic data — and to point out that a broad except Exception in a worker loop hides the failure entirely.

for a principal

Own the policy question: whether raising the ceiling belongs in your codebase at all, and whether input depth should instead be bounded where the data enters the system so that no component has to depend on a process-wide interpreter setting.

## The guard, not the hardware CPython does not let Python code recurse until the operating system's stack is exhausted. It maintains its own counter of how many Python frames are currently active on the calling thread, compares that counter against an interpreter-wide ceiling, and raises `RecursionError` when the next call would cross it. `sys.getrecursionlimit()` reports the ceiling; on a stock build it is **1000**, and that default is unchanged through 3.14. The reason the guard exists is blunt: a function with a missing base case would otherwise walk off the end of the thread's C stack, and a stack overflow is not an exception — it is a process-level crash with no traceback, no `finally` blocks, and no chance to log anything. Converting that into an ordinary Python exception is the whole point. You get a traceback pointing at the runaway function, the stack unwinds normally, `finally` blocks and context managers run, and the process survives. ## What the number actually counts Three misreadings are common, and all three are worth naming out loud: - It is **not** a byte size. It has no direct relationship to the thread's stack size in kilobytes. - It is **not** a count of calls to *your* function. It is the total depth of Python frames live on the thread, so everything already on the stack — the module or REPL frame you started from, the test runner, the framework layers under you — spends part of the budget before your recursion begins. - The ceiling is interpreter-wide but the **counter is per thread**. Each thread carries its own depth count against the same shared limit, so a worker thread starts fresh rather than inheriting the main thread's depth. Several everyday constructs quietly consume frames too. A decorator that wraps a call adds a frame per level. A generator expression runs in its own frame each time it is resumed. Since 3.12, list, set and dict comprehensions are inlined into the enclosing function (PEP 709) and no longer add one, which is a small but real change to how much headroom the same code has on 3.11 versus 3.14. Treat 1000 as an order of magnitude, never as an exact budget you can compute against. ## Where the exception sits in the hierarchy `RecursionError` subclasses `RuntimeError`, which subclasses `Exception`. It was split out of plain `RuntimeError` in Python 3.5 so that code could distinguish stack exhaustion from other runtime faults. The practical consequence runs the other way, though: any handler broad enough to catch `RuntimeError` — and certainly `except Exception` — catches it as well. A worker loop that wraps each unit of work in `except Exception: log_and_continue()` will turn a genuine runaway recursion into a stream of logged warnings rather than a visible failure. ## Reading the traceback A thousand identical frames would be unreadable, so CPython collapses repeated frames when formatting a traceback and prints the repeated block once, followed by a marker such as `[Previous line repeated 996 more times]`. That count is usually the fastest diagnostic you have: a single repeated line means straightforward runaway recursion, while an alternating pair of lines means mutual recursion between two functions. ## What hitting it usually means The overwhelmingly likely cause is a bug, not a limit that is too small: 1. A base case that is never reached — the classic being a decrement that skips the terminating value, or a condition testing the wrong variable. 2. A cycle in data you assumed was a tree: a reply that points back at an ancestor, a symlink loop, an object graph with a back-reference. 3. Mutual recursion between two functions where neither owns the terminating condition. 4. An accidental recursion — a property or `__getattr__` that reads the very attribute it is defining. Only after ruling those out is the depth genuinely a property of the input, which is the one case where changing the ceiling is a legitimate engineering decision rather than a way of postponing a crash. ## A second guard underneath Since 3.12 CPython also tracks the C stack separately from the Python frame count, and on 3.13 and 3.14 it checks the remaining C stack space directly. Recursion that passes through C code — building the `repr` of a deeply nested container, for example — can therefore raise `RecursionError` with a message like `Stack overflow (used 3906 kB)` even when the Python-level limit is set very high. Same exception type, different guard, and it is the one that catches people who raise the ceiling and assume they have bought unlimited depth.

  • Does a limit of 1000 mean a recursive function can nest exactly 1000 times?
    No. The counter covers every Python frame live on the thread, and the module, REPL or test-runner frames you started from already spend part of it. Decorator wrappers add a frame per level and a generator expression runs in its own frame, so real code bottoms out somewhat below 1000. Since 3.12, list, set and dict comprehensions are inlined and no longer cost a frame, so the same function has slightly more headroom on 3.14 than on 3.11.
  • Why does a RecursionError traceback not print a thousand lines?
    CPython collapses consecutive identical frames when it formats a traceback, printing the repeated block once followed by a marker such as `[Previous line repeated 996 more times]`. That keeps the output readable and the count is a useful clue: one repeated line means a single runaway function, while two alternating lines point at mutual recursion.
  • Will `except RuntimeError` catch a RecursionError?
    Yes. `RecursionError` subclasses `RuntimeError`, so both that handler and any `except Exception` swallow it. That matters in worker loops that catch broadly and retry: a genuine stack overflow becomes an invisible, endlessly retried failure instead of a crash somebody notices.

It is a turnstile counter at the door, not a measurement of how big the room is: CPython counts heads rather than checking whether the floor is about to give way.

saying these in an interview costs you the question

  • Describes the limit as the operating system stack size in bytes
  • Says RecursionError inherits from MemoryError or OverflowError
  • Thinks the depth counter is shared across all threads
  • Assumes exactly 1000 calls to your own function fit
  • Treats every RecursionError as a tuning problem, never a missing base case

context

open as a page

Does CPython perform tail-call elimination on a tail-recursive Python function?

level: middleimportance: should knowfreq 48%

basics

~10 s

No. CPython pushes a fresh frame for every call, tail position or not, so a tail-recursive function still counts against the recursion limit and still raises RecursionError. Rewrite that shape as a loop.

open as a page

Is sys.setrecursionlimit a safe fix for a RecursionError raised while walking a deep chat transcript?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Only once you have proved the depth is bounded by the data rather than by a cycle. A higher ceiling lifts the Python frame count, but recursion that runs through C code still hits a separate stack guard.

open as a page

Why can catching RecursionError inside a recursive walk silently truncate its output?

level: middleimportance: nice to knowfreq 22%

basics

~20 s

The handler swallows the failure at whatever depth it struck and returns whatever was collected so far. CPython unwinds cleanly, so nothing crashes and no error surfaces — the caller simply receives a short, wrong result.

open as a page