Skip to main content

pycore_freelist.h

Per-interpreter free lists for small, frequently-allocated objects. This header defines the data structures CPython uses to recycle deallocated objects instead of returning their memory to the system allocator immediately.

Why it exists​

Allocation and deallocation of small objects is a hot path in any Python workload. A tight loop that appends to a list, formats a float, or builds tuples allocates and frees thousands of heap objects per second. Calling pymalloc (or the system malloc) on every one of those churns the allocator's internal bookkeeping.

Free lists short-circuit this: when a PyFloatObject is deallocated its memory is pushed onto a per-interpreter stack. The next allocation pops that slot, patches the fields, and returns a fully-formed object without a single allocator call. pycore_freelist.h is where all those per-type stack descriptors live.

Reading: _Py_freelist and _Py_freelist_state​

// Include/internal/pycore_freelist.h (CPython 3.14)
struct _Py_freelist {
void *head; // top of the recycled-object stack; NULL when empty
int numfree; // current depth
int maxfree; // capacity; when numfree reaches maxfree, extras are freed
};

struct _Py_freelist_state {
struct _Py_freelist floats;
struct _Py_freelist tuples[PyTuple_MAXSAVESIZE]; // one list per size 1..20
struct _Py_freelist lists;
struct _Py_freelist dicts;
struct _Py_freelist frames;
struct _Py_freelist slices;
struct _Py_freelist contexts;
};

_Py_freelist_state is embedded inside PyInterpreterState so each sub-interpreter maintains its own recycling pool. Objects can never migrate between interpreters via the free list.

PyTuple_MAXSAVESIZE is 20. Tuples of different lengths are common enough that CPython keeps a separate stack for each length from 1 through 20, avoiding any size-check overhead at pop time.

Reading: allocation fast paths​

_PyFloat_FromDoubleNoOp and _PyTuple_FromArray illustrate the pattern. Both check the relevant free list first and only call PyObject_Malloc when the list is empty.

// Simplified from Objects/floatobject.c (CPython 3.14)
PyObject *
PyFloat_FromDouble(double fval)
{
struct _Py_freelist *fl = &_Py_INTERP_FREELIST(floats);
PyFloatObject *op;
if (fl->numfree > 0) {
op = (PyFloatObject *)fl->head;
fl->head = *(void **)op;
fl->numfree--;
_PyObject_Init((PyObject *)op, &PyFloat_Type);
} else {
op = PyObject_Malloc(sizeof(PyFloatObject));
if (!op) return PyErr_NoMemory();
_PyObject_Init((PyObject *)op, &PyFloat_Type);
}
op->ob_fval = fval;
return (PyObject *)op;
}

The deallocation mirror (float_dealloc) pushes the dying object back onto fl->head by overwriting its first pointer-sized word with the old head value before incrementing fl->numfree.

Reading: interpreter shutdown drain​

During Py_Finalize the interpreter walks every free list in _Py_freelist_state and calls the real deallocator on every cached slot. This ensures that objects on the free list are counted and reported correctly by memory-leak checkers.

// Simplified from Objects/tupleobject.c shutdown path
void
_PyTuple_Fini(PyInterpreterState *interp)
{
struct _Py_freelist_state *state = &interp->freelist_state;
for (int i = 1; i < PyTuple_MAXSAVESIZE; i++) {
PyTupleObject *p = state->tuples[i].head;
while (p) {
PyTupleObject *next = (PyTupleObject *)p->ob_item[0];
PyObject_GC_Del(p);
p = next;
}
state->tuples[i].numfree = 0;
state->tuples[i].head = NULL;
}
}

Status in gopy​

Not yet ported. In gopy, Go's garbage collector handles object reclamation automatically, so there is no structural equivalent to _Py_freelist. The objects/ package allocates PyFloatObject, PyTupleObject, and similar structs directly via Go struct literals and relies on the GC to collect them. If allocation pressure becomes measurable in benchmarks, a Go sync.Pool per type would be the natural analogue, but no such optimization has been introduced yet.