python/cpython
Memory
CPython has a layered memory architecture. At the bottom is malloc. Above that, a small-object allocator (pymalloc, or mimalloc for free-threaded builds) handles the bulk of PyObject allocation. The Python C API exposes three "domains" so that callers can pick the right layer:
| Domain | API | Underlying allocator |
|---|---|---|
PYMEM_DOMAIN_RAW |
PyMem_RawMalloc, PyMem_RawFree, … |
malloc() |
PYMEM_DOMAIN_MEM |
PyMem_Malloc, PyMem_Free, … |
pymalloc / mimalloc |
PYMEM_DOMAIN_OBJ |
PyObject_Malloc, PyObject_Free, … |
pymalloc / mimalloc |
Each domain can be replaced with a custom allocator at startup via PyMem_SetAllocator. All three are exposed in Include/cpython/pymem.h.
obmalloc (default)
Objects/obmalloc.c is CPython's hand-rolled small-object allocator. It allocates arenas (256 KiB each) from malloc, divides them into 4 KiB pools, and divides pools into fixed-size blocks based on the requested size class (8, 16, 24, … bytes up to 512 bytes).
graph TD
OS[OS / malloc] --> ARENA[Arena: 256 KiB]
ARENA --> POOL[Pool: 4 KiB]
POOL --> BLOCK1[Block size class N]
POOL --> BLOCK2[Block size class N]A request larger than 512 bytes falls through directly to malloc. Free lists are per-size-class and per-arena, which makes typical Python workloads (lots of small objects with short lifetimes) very fast.
PYTHONMALLOC=debug (or any debug build) wraps each block with a "deadbeef" canary plus a small bookkeeping header; free walks the canary to detect overruns. PYTHONMALLOC=malloc bypasses obmalloc entirely so that ASan/Valgrind see every allocation.
mimalloc (free-threaded)
The free-threaded build uses mimalloc (Microsoft's small-object allocator) vendored under Objects/mimalloc/. It's a per-thread heap with thread-local free lists and atomic cross-thread free; this is much friendlier for nogil workloads than obmalloc, which serializes through the GIL.
The selection happens in _PyObject_DebugMallocStats and the slot table — a default build links pymalloc; --disable-gil links mimalloc.
Per-object allocation
For most types, Python code calls one of:
PyObject_New(type, type)/PyObject_NewVar(type, type, n)— for non-GC types.PyObject_GC_New(type, type)/PyObject_GC_NewVar(...)— for GC-tracked types. Reserves an extraPyGC_Headimmediately before the object.- The type's
tp_allocslot — usually defaults toPyType_GenericAlloc, which calls one of the above.
Free goes through:
PyObject_Free,PyObject_GC_Del— match the new function used.- The type's
tp_freeslot. tp_deallocorchestrates the whole teardown: drop owned refs, calltp_free.
Free lists and small-object caches
Several built-ins keep their own freelists to avoid going through obmalloc on hot paths:
- Tuples (
Objects/tupleobject.c) — per-size freelist for small tuples. - Lists (
Objects/listobject.c) — empty-list freelist. - Frames (
Objects/frameobject.c) — per-thread frame freelist; combined with the in-data-stack frame allocation introduced in 3.11 (see Interpreter). - Floats (
Objects/floatobject.c) — single freelist forfloatobjects. - Dicts and dict keys (
Objects/dictobject.c) —_PyDictKeysObjectfreelist plus shared keys for instance dicts. - Generator/coroutine (
Objects/genobject.c) — generator freelist, since they're often short-lived.
Freelists are per-interpreter (PEP 684) and capped to a small size to bound footprint.
Small-int and string caches
Two small object caches are notable:
- Small ints —
intvalues from-5to256are pre-allocated and shared. Source:Objects/longobject.c(_PyLong_GetZero,_PyLong_GetOne, etc.). In 3.12+ these are per-interpreter, not process-wide. - Interned strings — every identifier-shaped string (attribute names, function names, the small ASCII strings that
PyUnicode_InternFromStringis called with) is stored in a per-interpreter dict and shared. SeeInternalDocs/string_interning.md.
Tracemalloc
Modules/_tracemalloc.c and Lib/tracemalloc.py implement a Python-visible allocation tracker. It hooks the three allocator domains and records up to N frames of the calling stack for every live allocation. Enabled by PYTHONTRACEMALLOC=N or python -X tracemalloc=N.
This is one of the more useful production debugging tools — see Debugging.
Arenas in the C heap
The runtime also has an arena allocator in Python/pyarena.c used by the parser. This is a bump-pointer allocator: every AST node lives in the parser's arena and is freed in one shot when parsing finishes. It is unrelated to the obmalloc "arena" terminology; the names just happen to collide.
Subinterpreter and free-thread implications
- Per-interpreter heaps — each
PyInterpreterStatehas its own freelists, small-int cache, and interned dictionary. Crossing an interpreter boundary requires explicit data-marshalling throughPython/crossinterp.c. - No globals — extension modules must store state on a
PyModuleObject(multi-phase init), not in C globals. Otherwise two subinterpreters share state in a way that violates isolation. - mimalloc heaps are per-thread under nogil, so cross-thread frees go through a thread-safe queue inside mimalloc.
Entry points for modification
- The default small-object allocator —
Objects/obmalloc.c. - Pluggable allocators / debug hooks —
Include/cpython/pymem.h,PyMem_SetAllocatormachinery inobmalloc.c. - Adding a freelist for a built-in type — search for "freelist" in
Objects/for examples. - Tracemalloc backend —
Modules/_tracemalloc.c. - Parser arena —
Python/pyarena.c.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.