Open-Source Wikis

/

CPython

/

Systems

/

Memory

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 extra PyGC_Head immediately before the object.
  • The type's tp_alloc slot — usually defaults to PyType_GenericAlloc, which calls one of the above.

Free goes through:

  • PyObject_Free, PyObject_GC_Del — match the new function used.
  • The type's tp_free slot.
  • tp_dealloc orchestrates the whole teardown: drop owned refs, call tp_free.

Free lists and small-object caches

Several built-ins keep their own freelists to avoid going through obmalloc on hot paths:

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 intsint values from -5 to 256 are 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_InternFromString is called with) is stored in a per-interpreter dict and shared. See InternalDocs/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 PyInterpreterState has its own freelists, small-int cache, and interned dictionary. Crossing an interpreter boundary requires explicit data-marshalling through Python/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

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Memory – CPython wiki | Factory