Backend / Python core / 34_stack_vs_heap.md

Stack and heap in Python

Updated 4 interview angles 6 min read source
On this page8
  1. Every object is on the heap
  2. Where the frames live
  3. Recursion: two limits
  4. Threads have stacks; coroutines do not
  5. What keeps heap objects alive
  6. Common misconceptions
  7. Related
  8. Interview angle

Stack and heap in Python

The usual answer, “variables live on the stack and objects on the heap”, is half right in CPython and misleading in the half that interviewers probe. Every Python object lives on the heap. What lives in a stack-like structure is the frame of each running function, and its local variables are only references into the heap.

Where those frames are kept has changed twice in recent versions, and it explains recursion limits, why a thread costs far more than a coroutine, and what a closure or generator keeps alive. Outputs are from Python 3.14.5 unless noted.

Reference counting, garbage collection and the allocator are in Python memory model — allocation, refcounting, GC.

Every object is on the heap

python
def area(w, h):
    result = w * h        # a new int object, allocated on the heap
    return result

a = area(3, 4)

3, 4 and 12 are all heap objects: int is an object with a header, so sys.getsizeof(1) is 28 bytes and sys.getsizeof(2**64) is 36. There are no unboxed values living in a stack slot the way a C int does. The names w, h and result are slots in the function’s frame, each holding a reference (a pointer) to an object on the heap.

So “pass by value or by reference?” has a precise answer: a call copies the references into the new frame’s slots, and both frames point at the same objects. Rebinding a name changes only that frame’s slot; mutating the object is visible through every reference (Argument Passing in Python: By Reference or By Value?).

Some heap objects are shared rather than allocated per use: small ints from -5 to 256 are cached (int("256") is int("256") is True, 257 is not), and since 3.12 None, True and the small ints are immortal (Mutable vs Immutable Types in Python).

Where the frames live

A frame holds what one running call needs: its local variable slots, the evaluation stack for intermediate values, the position in the bytecode, and a link to the caller’s frame. That chain of frames is the Python call stack, the one a traceback prints.

Version Python frames are kept in
up to 3.10 a heap-allocated frame object per call
3.11+ a per-thread “data stack”: chunks of memory used as a stack
any a frame object built only when something asks for one

Since 3.11 a call pushes a small internal frame onto the thread’s data stack and pops it on return, which is a large part of why 3.11 made calls faster. The full frame object you see from sys._getframe(), in a traceback or from a debugger is created lazily, only when something asks for it.

Since 3.12, a call from Python to Python does not use the C stack at all: the interpreter switches frames in a loop instead of recursing in C. The C stack is used only when C code calls back into Python (a C extension, repr of a container, sorted with a Python key function).

Recursion: two limits

python
sys.getrecursionlimit()      # 1000
def f(): return f()
f()                          # RecursionError: maximum recursion depth exceeded
  • The Python-level limit is a counter of Python frames, 1000 by default, and it protects nothing physical since 3.12: Python frames are not on the C stack. Raising it with sys.setrecursionlimit lets deep recursion run, at the cost of memory for the frames.
  • The C stack is real and fixed per thread. Recursion that goes through C, such as repr() of a list nested 200,000 deep, is now caught by measuring the actual stack: 3.14 reports Stack overflow (used 1952 kB) while getting the repr of an object, where 3.12 counted it against the recursion limit.

Python has no tail-call elimination, so a deep recursion that a functional language would run in constant space builds one frame per call. Turn it into a loop, or an explicit stack (a list used as a stack) for tree and graph walks.

Threads have stacks; coroutines do not

Each OS thread gets its own C stack, reserved when the thread starts: typically 8 MB of virtual address space on Linux and 1 MB on Windows, of which only the touched pages use physical memory. threading.stack_size() returns 0, meaning “the platform default”, and can set a different size for threads started afterwards.

A coroutine has no stack of its own. Its frame lives inside the coroutine object on the heap, just like a generator’s: an idle coroutine object is 192 bytes on 3.14.5. A suspended await costs a heap object and whatever its frame references, not a reserved stack, which is why a process can hold 100,000 waiting coroutines and not 100,000 threads (asyncio and threads).

What keeps heap objects alive

Because frames hold references, anything that holds a frame holds its objects:

  • A suspended generator or coroutine keeps every local it has bound (Generators).
  • A closure keeps the variables it captured, in cells, after the enclosing function has returned.
  • An exception traceback references the frames it passed through, and through them every local in them. Holding a caught exception (err = e stored on an object, or sys.exc_info() kept around) keeps all of it alive; Python deletes the except ... as e name at the end of the block for exactly this reason.

Common misconceptions

  • “Small values like ints live on the stack.” No: every value is a heap object; frames hold references.
  • “Local variables are freed when the function returns.” The frame’s references are dropped; the objects are freed only if nothing else references them, by reference counting.
  • “A RecursionError means the stack overflowed.” For Python-to-Python recursion since 3.12, it means the configured limit was hit; the C stack was never involved.
  • “Generators are cheap because they run on the stack.” The opposite: they are cheap because their frame is on the heap and they hold no stack.

Interview angle 4

  • “Stack or heap: where does a Python variable live?” — the object is always on the heap. The name is a slot in the function’s frame holding a reference to it; frames form the call stack, kept since 3.11 in a per-thread data stack rather than as one heap object per call.
  • “What does a RecursionError mean?” — that the Python frame counter hit sys.getrecursionlimit() (1000). Since 3.12 Python calls do not use the C stack, so it is a policy limit; a real C-stack overflow (deep repr, C extensions) is detected separately, by measuring the stack on 3.14.
    • Follow-up: “How do you process a 100,000-deep tree?” — iteratively, with an explicit stack, since Python has no tail-call elimination.
  • “Why can you run 100,000 coroutines but not 100,000 threads?” — every thread reserves its own C stack (megabytes of address space); a suspended coroutine is a heap object of about 200 bytes plus what its frame references.
  • “Why might a caught exception keep memory alive?” — its traceback holds the frames it passed through, and their locals. That is why except E as e deletes e when the block ends.