Why is sys.setrecursionlimit(1_000_000) not a safe way to allow much deeper recursion?
answer
- Two different resources, one setting
- The counter is not the stack
- Frames on the heap since 3.11
- C re-entry still costs real stack
- Size the thread before starting it
basics
~10 ssys.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.
solid answer
~50 sThere are two different resources here. `sys.setrecursionlimit()` changes the interpreter's frame counter, which is a guard; the machine stack the thread was given at creation is the real limit, and nothing in Python resizes it. Since 3.11 a Python function calling another Python function keeps its frame in heap-allocated chunks rather than on the C stack, so pure-Python recursion is far cheaper than it used to be, and 3.12 and 3.13 added a separate guard for C-level recursion plus checks against the thread's actual remaining stack. But anything that re-enters the interpreter through C — deep-copying nested objects, comparison callbacks, `repr` of a nested structure — still consumes real stack per level, and on a thread with a small stack that ends the process. If you truly need depth, run the work on a thread created after `threading.stack_size(...)` and raise the limit only as far as the measured workload needs.
code
python · 14 linesimport sys
import threading
sys.setrecursionlimit(25_000)
threading.stack_size(32 * 1024 * 1024)
def depth(n=0):
return n if n == 20_000 else depth(n + 1)
result = []
worker = threading.Thread(target=lambda: result.append(depth()))
worker.start()
worker.join()
print("reached depth", result[0])go deeper
Remember the core distinction: the limit is a counter the interpreter keeps, while the stack is memory the operating system reserved. Changing one does not change the other.
Explain the mechanics: where frames live since 3.11, what still re-enters C per level, and how threading.stack_size() plus a measured ceiling is the supported way to buy depth.
Demonstrate the operational judgement: measure the deepest real input, size the worker thread for it, and argue why an arbitrary large ceiling turns a fast catchable failure into an uncatchable abort.
Own the tradeoff: per-thread stack reservation multiplied across a worker pool is real address space, and depth driven by untrusted input is an availability risk that belongs in input validation, not in interpreter settings.
## Two resources, one number `sys.setrecursionlimit(n)` writes a single integer: the ceiling on how many Python frames the interpreter will let you stack. That is a **guard**. The thing being guarded is the thread's **machine stack** — a fixed-size region reserved when the thread is created, typically around 8 MiB for the main thread on Linux (`ulimit -s` shows it), around 1 MiB by default on Windows, and whatever the platform chose for a worker thread. Python cannot resize a stack that already exists. So raising the limit is a promise the interpreter may not be able to keep: you have moved the fence without moving the cliff. ## What changed, and why the danger is narrower than it used to be * **3.11** reorganized frames so that a Python function calling another Python function no longer needs a C stack frame per call; those frames live in heap-allocated chunks. Pure-Python recursion therefore costs heap, not stack, and goes much deeper before anything physical breaks. * **3.12** gave C-level recursion its own separate limit, decoupled from the Python frame ceiling, so `sys.setrecursionlimit()` no longer silently raises the ceiling on C re-entry as well. * **3.13** began checking the thread's actual remaining stack space rather than relying purely on a counted constant, so on supported platforms a genuine C-stack overflow now surfaces as `RecursionError` far more often than it once did. On **3.14** those protections are what you get. They make a hard crash much less likely than on 3.10 — but "less likely" is not "impossible", and the reasoning an interviewer wants is the reason they cannot be complete. ## What still costs real stack Anything that leaves the interpreter loop and comes back consumes C stack per level: * deep-copying or pickling a long chain of objects, where C code calls back into Python for each level; * computing `repr` or `str` of a deeply nested container, where the C container code re-enters Python for each element; * comparison or key callbacks invoked from C during a sort; * a C extension that calls a Python callback which calls back into the extension; * attribute-protocol loops, where each level passes through C-implemented lookup machinery. For those paths the limit you set is not the operative bound — the stack is. A million-frame ceiling does not mean a million levels of `deepcopy`; it means the guard will not stop you before the hardware does. ## Giving the work a bigger stack The supported way to buy depth is to give the *thread* more stack, not to give the counter a bigger number: * `threading.stack_size(size)` sets the stack size used by threads created **after** the call. It does not affect threads that already exist, and it cannot change the main thread's stack. * The size must be at least 32 KiB and, on many platforms, a multiple of 4096; an unacceptable value raises `ValueError`, and a platform that does not support setting it raises `RuntimeError`. * Call it, then create and start the `threading.Thread` that runs the deep work, and join it. Combine that with a limit raised only as far as measurement justifies. The method is: take the deepest legitimate input you must support, run it on the configured stack, add generous headroom, and hold the process to that number. Guessing a large round number is exactly the mistake the question is about. ## What raising the limit does not do It does not reserve memory, it does not make calls faster, and it does not fix an unterminated recursion — it just delays the report. Each additional frame still costs memory whether it lives on the heap or the stack, so a deep run trades a fast `RecursionError` for slow memory growth. And when the real stack does go, the failure is not an exception: the interpreter gives up with a fatal error and aborts. There is no `except` clause, no `finally`, and no cleanup on that path — which is why an unbounded ceiling is strictly worse than a bounded one that fires early. ## The honest summary Raise the limit when the depth is bounded, known and measured, and give the work its own thread with a stack sized for it. Treat an arbitrary large number as a code smell: it says the author did not know how deep the workload goes, and the interpreter is no longer able to tell them.
- Why must threading.stack_size() be called before the thread is started?A thread's stack is reserved by the platform at creation time and cannot be grown afterwards. `threading.stack_size(size)` only records the size to use for threads created after the call, so setting it after `start()` affects nothing. It also cannot change the main thread's stack, which is why deep work is pushed onto a worker.
- How would you pick a limit rather than guessing a round number?Measure. Establish the deepest legitimate input the system must accept, run it on the stack size you are willing to reserve per thread, record the depth actually reached, then set the ceiling above that with headroom and treat exceeding it as a defect. A number nobody measured is a number nobody can defend.
- If pure-Python frames live on the heap now, why keep the limit at all?Because it is the only thing that turns an unterminated recursion into a report instead of unbounded memory growth, and because plenty of nesting still passes through C. The ceiling gives you a fast, catchable failure at a known depth, which is far more operable than a process that swells and then dies.
Raising the limit is like painting the guard rail further out on a clifftop path: the walkable ground did not get any wider, you just moved the warning.
saying these in an interview costs you the question
- Says setrecursionlimit allocates a bigger stack
- Sets a million as a routine fix
- Claims a crash from deep recursion is always catchable
- Thinks threading.stack_size resizes the main thread
- Assumes pure-Python depth is free because frames moved to the heap
- Believes a higher limit makes recursion faster