skip to content

Depth Limits and RecursionError

CPython caps call depth, so an over-deep or accidentally infinite recursion raises RecursionError instead of crashing the process. Interviewers ask what the limit protects and when raising it is safe.

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

questions

4

What does Python's sys.getrecursionlimit() cap, and what happens when a call chain exceeds it?

level: juniorimportance: must knowfreq 48%

answer

  1. A ceiling on nested calls
  2. Counts frames per thread, not bytes
  3. Default value is one thousand
  4. Crossing it raises, not crashes
  5. Ask sys for the current ceiling

basics

~20 s

sys.getrecursionlimit() reports the ceiling on how many Python frames may be stacked at once, 1000 by default. When a call would go past it, CPython raises RecursionError instead of letting the thread run out of stack.

solid answer

~40 s

CPython keeps a per-thread count of how many Python frames are currently active. `sys.getrecursionlimit()` returns the ceiling on that count and `sys.setrecursionlimit(n)` moves it; the default has been 1000 through 3.14. When a call would push the count past the ceiling, the interpreter raises `RecursionError` at the call site rather than pushing the frame. `RecursionError` is a normal exception (a subclass of `RuntimeError`): it propagates, runs `finally` blocks, and can be caught. The counter measures frames, not self-calls, so a thousand-deep chain of *different* functions trips it too, and so do things with no visible recursion in your code, such as decoding deeply nested JSON or deep-copying a long chain of linked objects.

code

python · 10 lines
python
import sys

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

print("limit:", sys.getrecursionlimit())
try:
    depth()
except RecursionError:
    print("RecursionError raised, process still alive")

go deeper

for a junior

Recall the three facts: the default ceiling is 1000 nested Python frames, sys.getrecursionlimit() reads it, and going past it raises RecursionError rather than crashing the interpreter.

for a middle

Be ready to explain that the counter tracks frames rather than self-calls, that it is per thread, and that RecursionError is an ordinary catchable exception subclassing RuntimeError.

for a senior

Show the diagnostic habit: read the collapsed traceback, decide whether the cycle is a missing base case, a protocol loop, or genuinely deep data, and say why raising the limit is the last option rather than the first.

for a principal

Own the policy angle: whether services should ever mutate an interpreter-wide limit, how depth that scales with untrusted input becomes an availability risk, and where you bound input size instead.

## What the number actually is CPython maintains, for each thread, a counter of how many Python-level frames are currently stacked. `sys.getrecursionlimit()` returns the ceiling on that counter; `sys.setrecursionlimit(n)` moves the ceiling. On a stock interpreter the value is **1000**, and it has been 1000 for every 3.x release including **3.14**. When a call would push the counter past the ceiling, the interpreter does not push the frame. It raises `RecursionError` at the call expression instead. Two consequences follow immediately, and both come up in interviews: * **It is an ordinary exception.** `RecursionError` subclasses `RuntimeError`, so it propagates normally, runs `finally` blocks and `__exit__` methods on the way out, and can be caught. It is not a process crash, and it is not the same thing as the machine-level stack overflow that other runtimes give you. * **It is a guard, not the resource.** The limit exists so that unbounded recursion produces a Python-level error you can see in a log, rather than exhausting the thread's real stack and killing the process. Raising the number does not buy you memory; it only moves the guard. ## It counts frames, not self-calls The counter has no idea whether a function is calling itself. A chain of a thousand *different* nested calls raises `RecursionError` just as readily, which is why mutual recursion (`a()` calls `b()` calls `a()`) is bounded by the same number. It also explains the cases where the traceback contains no recursive function of yours at all: * decoding deeply nested JSON with `json.loads`, where each nesting level is a nested call; * `copy.deepcopy` of a long chain of objects that each reference the next; * computing the `repr` of a structure that (directly or indirectly) contains itself; * an attribute-delegation loop through `__getattr__`, where the recursion is in the object protocol rather than in your control flow. ## Frames you did not write Not every construct that looks like nesting costs a frame, and the answer changed recently. Since **3.12** a list, dict or set comprehension is compiled inline into the function that contains it, so it no longer costs a frame of its own; a **generator expression** still gets one, because it must be suspended and resumed independently. Each resumption of a generator or coroutine also pushes a frame while it runs. None of this matters at depth ten and all of it matters at depth nine hundred, which is why a function that was comfortably inside the ceiling on 3.10 can sit at a different distance from it on 3.14. ## Reading the traceback A `RecursionError` traceback does not print a thousand identical frames. CPython collapses the repeat and prints a line such as `[Previous line repeated 993 more times]`. The two or three lines that repeat are the cycle, and they are the diagnosis: if they are your recursive function, look at the base case; if they are a dunder method, you have a protocol loop rather than deep data. ## Setting the limit `sys.setrecursionlimit(n)` is interpreter-wide, not scoped to the caller, so a library that sets it changes behaviour for the whole program. Lowering it below the depth you are currently at fails loudly rather than silently: the call itself raises `RecursionError` with a message naming the current depth. Raising it reserves nothing and speeds nothing up; it only permits more frames, each of which still costs memory, and it moves the guard closer to (or past) the real stack. ## Threads The depth counter is per thread: a worker thread starts at depth zero regardless of how deep the thread that spawned it was. The *limit value* is shared by the whole interpreter, while the actual stack each thread has is a separate, platform-level quantity fixed when the thread is created. ## The judgement an interviewer is listening for When you see `RecursionError`, the first question is never "what should I raise the limit to". It is **"is this a bug or is this deep data?"** A missing or unreachable base case, an off-by-one that never converges, or an attribute-delegation loop is a defect, and raising the limit converts a fast failure into a slow one. Only when the depth is genuinely proportional to legitimate input — a recursive walk over a degenerate, list-shaped tree, for example — is changing the limit even a candidate, and even then it is a bounded workaround rather than a fix, because a limit that is adequate for today's largest input is not adequate for an input twice that size. Depth 1000 is comfortable for anything balanced, where depth grows with the logarithm of the size; it is inadequate the moment depth grows linearly with the input. That distinction, more than the exact number, is what the question is really testing.

  • Is the recursion depth counter shared across threads?
    No. Each thread counts its own active frames and starts at zero, so a deep main thread does not shrink what a worker may do. The *limit value* set by `sys.setrecursionlimit()` is interpreter-wide, and the real stack a thread gets is a separate platform quantity fixed when the thread starts.
  • Why does a RecursionError traceback not show every frame?
    CPython collapses the repeating section and prints a line such as `[Previous line repeated 993 more times]`. That keeps the output readable and, more usefully, isolates the cycle: the handful of lines that repeat are the recursion, and the innermost frames show where it finally hit the ceiling.
  • Can you get RecursionError from code that contains no recursive function?
    Easily. Decoding deeply nested JSON, deep-copying a long chain of linked objects, or computing the `repr` of a self-referential structure all nest calls internally. So does an attribute-delegation loop through `__getattr__`. The recursion lives in the data or in the object protocol rather than in your control flow.

saying these in an interview costs you the question

  • Says the limit measures stack memory in bytes
  • Thinks RecursionError means the process crashed
  • Believes only self-calling functions can trip it
  • Assumes raising the limit is always safe
  • Claims the limit is per-process rather than per-thread depth
  • Treats RecursionError as uncatchable

context

open as a page

Why is sys.setrecursionlimit(1_000_000) not a safe way to allow much deeper recursion?

level: middleimportance: should knowfreq 40%

basics

~10 s

sys.setrecursionlimit() moves CPython's own frame counter; it does not enlarge the thread's machine stack. Push far past what that stack really holds and the interpreter can die outright instead of raising a catchable RecursionError.

open as a page

A proxy whose __getattr__ delegates to self._client raises RecursionError when deep-copied — why?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Copying builds an instance without running init, so _client is absent. Probing that blank object for a hook name falls through to getattr, which reads self._client, which is also absent, so getattr calls itself forever.

open as a page

Why is catching RecursionError an unreliable way to recover from runaway recursion?

level: seniorimportance: nice to knowfreq 16%

basics

~20 s

When RecursionError is raised you are still at maximum depth, with only a small margin of extra frames. Handlers, finally blocks and finalizers run inside that margin, so cleanup can overflow again and the interpreter aborts outright.

open as a page