skip to content

Live Call Stack

The stack of frames a running program actually has: what each frame exposes to code that asks, how deep the interpreter will let you go, and what happens at the limit.

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

questions

7

Which CPython limit decides when deep recursion raises RecursionError?

level: juniorimportance: must knowfreq 62%

answer

  1. A counter, not a memory budget
  2. One number for the whole interpreter
  3. Default is one thousand frames
  4. sys.getrecursionlimit and sys.setrecursionlimit
  5. RecursionError subclasses RuntimeError

basics

~10 s

CPython counts how many Python frames are stacked on the current thread and raises RecursionError once that count passes the interpreter's recursion limit, which is 1000 by default. sys.getrecursionlimit() reads it; sys.setrecursionlimit() changes it.

solid answer

~40 s

CPython keeps a per-thread count of how many Python-level frames are currently stacked. Before entering another Python call it compares that count with one interpreter-wide ceiling, the recursion limit, and raises `RecursionError` ("maximum recursion depth exceeded") when the ceiling is crossed. The default on CPython is 1000; `sys.getrecursionlimit()` reads the value and `sys.setrecursionlimit(n)` replaces it for the whole interpreter, not just the calling function. Two things it is not: it is not a byte budget, because it counts frames rather than memory, and it is not per-thread as a *value* — every thread compares its own depth counter against the same number. `RecursionError` subclasses `RuntimeError`, so a broad `except RuntimeError` will swallow it. Deep data, an accidental cycle, or a `__getattr__` that touches a missing attribute on `self` all trip it.

code

python · 11 lines
python
import sys

print(sys.getrecursionlimit())

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

try:
    depth()
except RecursionError:
    print("RecursionError raised by the frame counter")

go deeper

for a junior

Recall the default of 1000, the two functions sys.getrecursionlimit() and sys.setrecursionlimit(), and that the error raised is RecursionError. Be able to say it counts calls, not bytes.

for a middle

Explain the mechanics: a per-thread frame counter compared against one interpreter-wide ceiling, why the guard exists at all, and why a tail-recursive rewrite does not help in CPython.

for a senior

Demonstrate that your first move on a RecursionError is deciding whether the recursion is unbounded or merely deep, and that you read the traceback's repeated-frame marker instead of reaching for a bigger number.

for a principal

Own the policy: whether services in your estate may change an interpreter-wide setting at all, and what depth bound you put on parsers and walkers that ingest untrusted or machine-generated structures.

## What the limit actually is Every Python-level call pushes a frame — the object that holds the running function's locals, its instruction pointer and a link to its caller. CPython tracks how deep that chain currently is for the running thread, and before it enters another Python call it compares that depth against a single ceiling stored in the interpreter: the *recursion limit*. Cross it and CPython raises `RecursionError` with the message "maximum recursion depth exceeded". The default value on CPython is **1000**. Two functions expose it, and both live in `sys`: ```python import sys print(sys.getrecursionlimit()) # 1000 on a stock CPython sys.setrecursionlimit(3000) # interpreter-wide, effective immediately ``` `sys.setrecursionlimit` is not scoped to a call, a module or a thread. It mutates one number that the whole interpreter consults, so a library that raises it raises it for everyone in the process, and there is no context manager that puts it back. Code that changes it should save the old value and restore it. ## What it is not It is **not a memory limit**. It counts frames, and frames vary enormously in size — a function with two locals and a function holding a large tuple of intermediates cost the same one unit. So the limit does not tell you how much memory a recursion will use, and hitting it is not the same failure as `MemoryError`. It is **not a per-thread setting**. Each thread carries its own depth counter, so one thread recursing deeply does not push another over the edge, but they all measure themselves against the same limit value. It is **not a language rule**. It is a CPython implementation guard; other implementations are free to enforce depth differently, and no Python implementation is required to eliminate tail calls, so "write it tail-recursively" does not make the depth go away in CPython. ## Why the guard exists Historically, each Python-level call also consumed a frame on the real C stack that the operating system gave the thread. That stack is a fixed allocation — commonly around 8 MiB for a process's main thread — and running off the end of it is a hard crash, not an exception. The recursion limit is a cheap counter that is designed to trip *before* the real stack is exhausted, converting a segfault into a catchable Python exception. Since **3.11** pure Python-to-Python calls are inlined in the evaluation loop and no longer cost a C stack frame each, and **3.12** split the accounting outright: the number you set governs Python frames, while recursion that re-enters the interpreter through C code is bounded by a separate internal guard. In other words, the limit you can see and set is a budget for *Python* frames. ## Where it fires in real code A literal recursive function on deep data is the obvious case: a linked structure with several thousand links walked one call per link will exceed 1000 long before the data looks large. The subtler cases have no visible recursion at all: * a `__getattr__` that reads `self.something` which is itself missing, so every lookup re-enters `__getattr__`; * a `__repr__` that formats a structure containing itself; * a property getter that reads the property it implements; * two functions that call each other with no terminating condition, so the cycle is spread across files. The traceback is a good tell. The `traceback` machinery collapses repeats, so you see a short cycle of frames followed by a line such as `[Previous line repeated 996 more times]`. A tight repeat of one or two distinct frames means runaway recursion; a long chain of *different* frames means genuinely deep data. ```python import sys def depth(n=1): return depth(n + 1) try: depth() except RecursionError: print("stopped at limit", sys.getrecursionlimit()) ``` ## Catching it `RecursionError` is a subclass of `RuntimeError`, which is why an over-broad `except RuntimeError` quietly absorbs it. Catching it deliberately is legitimate for a probe — measuring how deep you got, or turning a hostile input into a 400-style rejection — but it is a poor control-flow mechanism, because the handler runs while the stack is still almost full and needs frames of its own to do anything useful. ## The first decision after seeing one Ask whether the recursion is *unbounded* or merely *deep*. Unbounded means a cycle or a missing base case, and no limit value fixes it — raising the ceiling only makes the failure slower and more expensive. Deep means the input legitimately has that many levels, and then the choice is between raising the ceiling knowingly and restructuring the traversal so depth does not live on the call stack at all.

  • A colleague says Python has no recursion problem because their function is tail-recursive. What do you say?
    CPython does not eliminate tail calls, and the language does not require any implementation to. A tail-recursive function pushes a frame per call exactly like any other, so it counts against the recursion limit identically. Writing the recursion in tail position changes nothing about depth in CPython; only turning the recursion into iteration, or raising the limit, changes the outcome.
  • Can RecursionError appear from code that contains no recursive function at all?
    Yes, and it is the common production shape. A `__getattr__` that reads a missing attribute on `self` re-enters itself; a `__repr__` that formats a self-referential structure recurses through formatting; a property getter that reads its own property loops. None of these look recursive at the call site — the traceback's repeated-frame marker is what identifies them.
  • Does each thread get its own recursion limit?
    Each thread has its own depth counter, so one thread's deep call chain does not consume another's headroom. The limit *value*, however, is one interpreter-wide setting: `sys.setrecursionlimit` in any thread changes the ceiling every thread is measured against. What differs per thread is the real stack it was given by the OS, which is a separate constraint from the counter.

It is a turnstile counter at the door, not a measure of how full the room is: it counts how many calls went in, and never asks how big any of them was.

saying these in an interview costs you the question

  • Says the recursion limit is measured in bytes of memory
  • Believes CPython eliminates tail calls so depth never matters
  • Thinks sys.setrecursionlimit only affects the calling function
  • Claims each thread has its own separately settable limit value
  • Confuses RecursionError with MemoryError or StackOverflowError
  • Assumes RecursionError always means a bug in a recursive function

context

open as a page

How can a Python function find out which function called it, using frame objects?

level: juniorimportance: should knowfreq 30%

basics

~10 s

Get a frame object with inspect.currentframe() or sys._getframe(), follow its f_back attribute to the caller's frame, then read f_code.co_name for the caller's name and f_lineno for the call site.

open as a page

Why does assigning into locals() inside a Python function not change the variable?

level: middleimportance: should knowfreq 26%

basics

~20 s

Function locals live in numbered slots on the frame, not in a dictionary. locals() manufactures a snapshot dict of those slots, so writing into it changes only the copy. Since 3.13, a frame's f_locals is a write-through proxy and does update the real variable.

open as a page

Why can raising sys.setrecursionlimit crash the process instead of curing RecursionError?

level: middleimportance: should knowfreq 48%

basics

~20 s

sys.setrecursionlimit only moves CPython's frame counter. The real constraint is the fixed stack the operating system gave the thread, so lifting the counter past what that stack supports turns a catchable RecursionError into a hard crash with no traceback.

open as a page

Why does calling inspect.stack() in a per-document logging helper blow a rebuilder's latency budget?

level: seniorimportance: should knowfreq 34%

basics

~20 s

inspect.stack() walks every frame to the top of the stack, builds a record for each, and by default reads a source line per frame through the line cache. Cost scales with stack depth and touches the filesystem, so it is thousands of times slower than sys._getframe(1).

open as a page

A nightly subscription-billing run dies with RecursionError — how do you tell runaway recursion from legitimately deep data?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Read the traceback's repeated-frame marker: a short cycle of one or two frames repeating means a runaway loop, while a long chain of different frames means deep data. Confirm by re-running the failing record with a lowered limit and a depth counter.

open as a page

When would you use threading.stack_size to run a deeply recursive routine?

level: seniorimportance: nice to knowfreq 14%

basics

~20 s

When depth is genuine and the recursive code cannot be rewritten. threading.stack_size sets the stack for threads created afterwards, so you give a worker thread a large stack, raise the recursion limit, and run the deep work there rather than on the main thread.

open as a page