skip to content

What does threading.stack_size do, and how does it let deeper recursion run in a Python worker thread?

level: middleimportance: nice to knowfreq 16%

answer

  1. Two different ceilings, only one is memory
  2. Applies to threads started afterwards
  3. The main thread cannot be resized
  4. Pair it with the frame-count ceiling
  5. Reserved per thread, so a pool multiplies it

basics

~20 s

threading.stack_size(size) sets the stack size requested for threads started after the call, and returns the previous value. Pair it with a raised sys.setrecursionlimit() to run deep recursion in that worker; the main thread's stack is fixed by the OS.

solid answer

~50 s

`threading.stack_size()` with no argument reports the size the interpreter will request for new threads — `0` means the platform default — and with an argument it sets it and returns the old value. It affects **only threads created after the call**, never a running thread and never the main thread, whose stack the operating system fixed at process start. The size must be at least 32 KiB and is normally a multiple of 4096; anything else raises `ValueError`. It is one half of the deep-recursion pattern: raising `sys.setrecursionlimit()` alone only moves the interpreter's frame ceiling, so the classic recipe is to call `threading.stack_size()` with a generous value, start a `threading.Thread`, and raise the recursion limit inside it. The better answer in most interviews is still to rewrite the recursion iteratively — reach for this when the recursion is genuinely unavoidable.

code

python · 15 lines
python
import sys
import threading

def countdown(n):
    return 0 if n == 0 else countdown(n - 1)

def run():
    sys.setrecursionlimit(60_000)   # process-wide, even from a worker
    print(countdown(50_000))

previous = threading.stack_size(64 * 1024 * 1024)
worker = threading.Thread(target=run)
worker.start()
worker.join()
threading.stack_size(previous)       # restore the default for later threads

go deeper

for a junior

You are not expected to have used this. Recall that a thread has a fixed stack, that the interpreter's recursion limit is a separate counter, and that rewriting recursion as a loop avoids both.

for a middle

Explain the mechanics: the call sets the size requested for threads started afterwards, returns the previous value, rejects sizes under 32 KiB, and pairs with a raised recursion limit because the two ceilings are independent.

for a senior

Show judgment about scope and cost — restoring the previous value so a worker pool does not inherit a huge reservation, confining the raised recursion limit's blast radius, and preferring an explicit-stack rewrite where the code is yours to change.

for a principal

Frame it as a last resort with process-wide side effects: a shared recursion ceiling and a shared thread-stack default are global state, so decide whether depth-bounded input validation is the cheaper control before any of this reaches production.

## What the call does `threading.stack_size()` is a module-level function that reads or writes the stack size the interpreter asks the operating system for **when it creates the next thread**. - Called with no argument it returns the current setting; `0` means "whatever the platform's default is". - Called with a size in bytes it sets that value and returns the previous one. - The size must be `0` or at least 32 KiB, and in practice a multiple of 4096; a bad value raises `ValueError`, and a platform that cannot honour the request raises `RuntimeError`. The scope of the setting is the thing candidates most often get wrong. It is **process-wide state that applies to threads started later**. It does not resize a thread that is already running, and it has no effect at all on the **main thread**, whose stack was sized by the operating system and the linker before Python started executing. ## Why it exists: two independent ceilings Deep recursion runs into two separate limits, and confusing them wastes a lot of debugging time. 1. **The interpreter's frame ceiling**, read and written with `sys.getrecursionlimit()` and `sys.setrecursionlimit()`. It counts nested Python frames and defaults to 1000. It is a counter, not memory. 2. **The real machine stack of the thread**, sized when the thread is created. This is where C-level call frames actually live. Raising only the first ceiling moves the counter without giving the thread more room. `threading.stack_size()` is how you influence the second one — but only for a thread you create yourself. That is the whole reason the deep-recursion recipe involves a worker thread at all: it is the only stack in the process whose size Python can ask for. ## The pattern Set the stack size, start a thread, raise the recursion limit inside it, do the deep work there, and join. Remember that `sys.setrecursionlimit()` is process-wide even though the big stack belongs to one thread — there is no per-thread recursion limit — so you are relaxing a guard rail for every thread while only one of them has the extra room. Keep the deep work confined to the worker. ## When it actually matters on modern CPython Since **3.11**, CPython inlines Python-to-Python calls, so a pure-Python recursive call no longer consumes a C stack frame each time; and since **3.12** the recursion limit applies to Python code while C-level recursion is protected by a separate mechanism that stops it crashing the virtual machine. On **3.14**, runaway recursion — including recursion that crosses C code, such as `repr()` on a deeply nested list — surfaces as a clean `RecursionError` rather than the segfault older write-ups promise. So the honest framing is: a bigger thread stack buys headroom for recursion that **crosses C code repeatedly** — recursive dunder dispatch, comparison or `repr` on deeply nested structures, a recursive-descent parser whose grammar bottoms out in C-implemented calls. For flat pure-Python recursion on 3.14 you will often find that raising `sys.setrecursionlimit()` alone gets you further than it used to. ## The costs A thread stack is reserved address space per thread. Asking for 64 MiB is nearly free on a 64-bit machine for one worker — the pages are not touched until used — but the setting applies to **every thread started after the call**, so a pool that spins up dozens of workers reserves that size dozens of times, and on a memory-constrained deployment that reservation is not free. Set the size immediately before creating the thread that needs it and set it back afterwards, rather than leaving a large value as the process-wide default. There is also a portability caveat: the platform may round the request, impose its own minimum, or refuse it outright, so treat a large value as a request rather than a guarantee, and test on the platform you deploy to. ## What a good answer concludes with This is a genuinely useful escape hatch and a poor default. If the recursion depth is a function of input you control, bound the input. If it is a function of data structure shape, rewrite the walk with an explicit stack, which moves depth to the heap and removes both ceilings from the discussion. Reach for `threading.stack_size()` when the recursion is in code you cannot restructure — a third-party parser, a generated visitor — and you need the depth today. Saying that out loud is what separates a candidate who has read about the trick from one who has decided when to use it.

  • Why can't you use threading.stack_size() to give the main thread a bigger stack?
    Because the main thread's stack is created by the operating system when the process starts, before any Python code runs, and its size comes from the system limit and the executable's settings. `threading.stack_size()` only influences the argument Python passes when it asks the platform to create a *new* thread. If you need a large stack, the deep work has to happen on a thread you create yourself.
  • If you set a 64 MiB thread stack, does the process immediately consume 64 MiB more memory?
    No — it reserves that much address space for the thread, and pages become resident only as the stack is actually used, so a single worker costs almost nothing on a 64-bit machine. The trap is scope: the setting applies to every thread started afterwards, so a pool of dozens of workers reserves it dozens of times. Set it right before creating the thread that needs it and restore the previous value afterwards.
  • You raised sys.setrecursionlimit() and the recursion still fails. What else is in play?
    The frame-count ceiling is only one of the two limits. The thread still has a fixed machine stack, and CPython also guards C-level recursion separately, so recursion that repeatedly crosses C code can still raise `RecursionError` well before your new ceiling. Either give the work a thread created after a `threading.stack_size()` call, or restructure the walk to use an explicit stack instead of the call stack.

saying these in an interview costs you the question

  • Thinking it resizes the main thread or a running thread
  • Assuming sys.setrecursionlimit alone provides more stack
  • Believing the limit can be set per thread
  • Leaving a huge stack size as the process-wide default
  • Treating the requested size as guaranteed by every platform
  • Reaching for it before considering an iterative rewrite

context