Reference Counting Mechanics
This note dissects the memory-layer machinery of CPython reference counting as it exists in 3.14: the exact bytes of the reference count inside the object header (which were repacked in 3.14), the line-by-line bodies of the
Py_INCREFandPy_DECREFmacros, how immortal objects are detected by those macros, and what actually happens at zero —_Py_Deallocreaching throughtp_deallocto destroy the object and return its bytes to the allocator (CPythonInclude/refcount.h, 3.14.0;Objects/object.c, 3.14.0). It is one of three lenses on refcounting and is deliberately the most low-level: Reference Counting is the object-model overview (the concept, deterministic destruction, the GIL connection) and Reference Counting in C Extensions is the C-API discipline (borrowed vs owned references that extension authors must obey). Read those for the why and the how-to; read this for what the machine actually does.
To avoid duplicating its siblings, this note does not re-derive the “count of pointers, freed at zero” mental model (Reference Counting does that) nor re-argue the motivation for immortal objects (that note does that). It starts one level down: at the field layout and the generated code.
Mental Model — The Field, the Two Macros, the Zero Trigger
The whole mechanism is three moving parts: a counter field embedded in every object’s header, a pair of inline functions that mutate it, and a deallocation routine that fires when a decrement lands on zero.
flowchart TD H["Object header (64-bit, 3.14):<br/>uint32_t ob_refcnt | uint16_t ob_overflow | uint16_t ob_flags<br/>+ PyTypeObject* ob_type<br/>(one 64-bit word holds all three sub-fields)"] INC["Py_INCREF(op)"] --> CHK1{"ob_refcnt >=<br/>_Py_IMMORTAL_INITIAL_REFCNT<br/>(3 << 30)?"} CHK1 -- "yes (immortal)" --> NOP1["return (no write)"] CHK1 -- "no" --> ADD["ob_refcnt = ob_refcnt + 1"] DEC["Py_DECREF(op)"] --> CHK2{"_Py_IsImmortal(op)?<br/>(int32_t)ob_refcnt < 0"} CHK2 -- "yes" --> NOP2["return (no write)"] CHK2 -- "no" --> SUB["--ob_refcnt"] SUB --> Z{"== 0?"} Z -- "no" --> DONE["done"] Z -- "yes" --> DEAL["_Py_Dealloc(op):<br/>type->tp_dealloc(op)<br/>-> free bytes to allocator"] ADD --> H SUB --> H
Diagram: the 3.14 object header (top) packs the 32-bit ob_refcnt, a 16-bit ob_overflow, and 16-bit ob_flags into one machine word; Py_INCREF and Py_DECREF mutate ob_refcnt with an immortal short-circuit on each path, and a decrement to zero triggers _Py_Dealloc. The insight: the two checks use different thresholds (incref tests against 3<<30, decref against the sign bit 1<<31), and that asymmetry is deliberate self-healing headroom — explained below.
The Object Header and the 3.14 Refcount Repack
Where the count lives changed shape in 3.14, and getting this right matters because it determines the width of the count and how immortality is detected. In 3.13 and earlier, the refcount was a single full-width signed field, Py_ssize_t ob_refcnt — 64 bits on a 64-bit platform. In 3.14, on 64-bit platforms, the same 64-bit word is repacked into three sub-fields via an anonymous union (Include/object.h, 3.14.0):
struct _object {
union {
#if SIZEOF_VOID_P > 4
PY_INT64_T ob_refcnt_full; /* the whole 64-bit word, for atomic init */
struct {
# if PY_BIG_ENDIAN
uint16_t ob_flags;
uint16_t ob_overflow;
uint32_t ob_refcnt;
# else
uint32_t ob_refcnt; /* the actual reference count: 32 bits */
uint16_t ob_overflow;
uint16_t ob_flags;
# endif
};
#else
Py_ssize_t ob_refcnt; /* real 32-bit platforms: full-width count */
#endif
};
PyTypeObject *ob_type;
};Walking this structure field by field:
- The outer
unionlets the same 64 bits be addressed two ways: as onePY_INT64_T ob_refcnt_full(used to initialize or copy all three sub-fields in a single store), or as the inner anonymousstructof three named fields. The anonymous struct means you writeop->ob_refcntdirectly, notop->u.s.ob_refcnt. uint32_t ob_refcntis the reference count proper — now 32 bits wide, not 64. On a little-endian machine it occupies the low 32 bits of the word (which is why the endian branch reorders the fields: the count must stay in the low half regardless of byte order).uint16_t ob_overflowanduint16_t ob_flagsare the two new 16-bit fields that share the upper half of the word with… nothing the old layout used. Verified againstrefcount.h@ v3.14.5,ob_flagsholds exactly two object-level flag bits:_Py_IMMORTAL_FLAGS(value 1) and_Py_STATICALLY_ALLOCATED_FLAG(value 4), combined as_Py_STATIC_FLAG_BITS;_Py_STATIC_IMMORTAL_INITIAL_REFCNTshifts those bits by<< 48into theob_flagsslot of the 64-bit word.ob_overflowis unused reserved padding — greppingobject.h,refcount.h, andObjects/object.cat v3.14.5 finds only its declaration, never a read or write; it exists so the packed struct lines up with the 64-bitob_refcnt_fullunion member. These fields existed nowhere in 3.13 — the oldPy_ssize_t ob_refcntused the whole word for the count.
Resolved (2026-06-01)
ob_flagsbit enumeration andob_overflow’s (non-)role verified againstInclude/refcount.h,Include/object.h, andObjects/object.cat v3.14.5. Only two object-level flags exist (_Py_IMMORTAL_FLAGS=1,_Py_STATICALLY_ALLOCATED_FLAG=4); these are distinct from the type-levelPy_TPFLAGS_*intp_flags.ob_overflowis dead padding.
The motivation for narrowing the count to 32 bits and adding the sibling fields, per the object.h and refcount.h comments, is twofold: it gives immortality a sign-bit detection scheme on the low 32 bits (below), and it lets the runtime initialize an object’s refcount-plus-flags in a single 64-bit store (ob_refcnt_full), which is cheaper on architectures like ARM. A 32-bit count still allows ~2 billion live references before saturation, which is far beyond any real object’s reference count.
On a genuine 32-bit platform (SIZEOF_VOID_P <= 4), the repack does not apply — the #else branch keeps ob_refcnt as a full-width Py_ssize_t. This is an important distinction the literature routinely blurs: “the 32-bit ob_refcnt” on a 64-bit build (a 32-bit field inside a 64-bit word) is not the same thing as “a 32-bit platform” (where ob_refcnt is the platform-width Py_ssize_t). The two cases use different immortal constants, shown next.
In the free-threaded build (Py_GIL_DISABLED, PEP 703) the header is entirely different — there is no single ob_refcnt at all but a split count plus locking/GC metadata (Include/object.h, 3.14.0):
struct _object {
uintptr_t ob_tid; /* owning thread id (for biased refcounting) */
uint16_t ob_flags;
PyMutex ob_mutex; /* per-object lock */
uint8_t ob_gc_bits; /* GC state bits */
uint32_t ob_ref_local; /* fast, owner-thread-only count */
Py_ssize_t ob_ref_shared; /* atomic count for non-owner threads */
PyTypeObject *ob_type;
};This split is the basis of Biased Reference Counting and is taken up in the divergence section below.
Py_INCREF — Line by Line
Taking a new strong reference calls Py_INCREF(op) (the C-API contract: “Indicate taking a new strong reference to object o … should not be destroyed,” C-API: refcounting). The core of the inline body on the default 64-bit GIL-enabled build is (refcount.h, 3.14.0):
PY_UINT32_T cur_refcnt = op->ob_refcnt; // (1) read the 32-bit count
if (cur_refcnt >= _Py_IMMORTAL_INITIAL_REFCNT) { // (2) immortal? (>= 3<<30)
_Py_INCREF_IMMORTAL_STAT_INC(); // (3) bump a stats counter (no-op in release)
return; // (4) leave count untouched
}
op->ob_refcnt = cur_refcnt + 1; // (5) otherwise, add one and store back- Read
ob_refcntinto a local. Because this is the GIL-enabled build, the read needs no atomicity — the GIL guarantees no other thread mutates this field concurrently. That non-atomic read-modify-write is precisely what makes refcounting cheap, and exactly what free-threading had to redesign. - Compare against
_Py_IMMORTAL_INITIAL_REFCNT, which is(3ULL << 30)=0xC0000000= 3,221,225,472 on 64-bit. If the count is at or above this, the object is immortal. _Py_INCREF_IMMORTAL_STAT_INC()increments an internal statistics counter when CPython is built with--enable-pystats; in a normal release build it expands to nothing.- Return without writing — the count is frozen. This is the entire point of immortality: no write means no cache-line dirtying, no copy-on-write fault, no atomic needed. The why is Immortal Objects; the mechanism is this branch.
- The common path: store
cur_refcnt + 1. A plainuint32_tadd. (The actualrefcount.huses saturated arithmetic guarded so an immortal value cannot wrap, but for a mortal object well below3<<30this is just+1.)
Py_DECREF — Line by Line, and the Zero Trigger
Releasing a strong reference calls Py_DECREF(op): “Once the last strong reference is released (i.e. the object’s reference count reaches 0), the object’s type’s deallocation function … is invoked” (C-API: refcounting). The simplified release-build body is (refcount.h, 3.14.0):
if (_Py_IsImmortal(op)) { // (1) immortal? -> never freed
_Py_DECREF_IMMORTAL_STAT_INC();
return; // (2) leave untouched
}
_Py_DECREF_STAT_INC();
if (--op->ob_refcnt == 0) { // (3) decrement; did it hit zero?
_Py_Dealloc(op); // (4) last reference gone -> destroy NOW
}_Py_IsImmortal(op)is the immortality test (defined below). Note it is a different test fromPy_INCREF’s threshold check — this is the deliberate asymmetry.- Immortal → return without writing, exactly as incref.
- Pre-decrement
--op->ob_refcntand compare the result to zero. If still positive, the object stays alive and we are done. - If zero, call
_Py_Dealloc(op)— the object is destroyed in this call, synchronously. Nothing is deferred to a later sweep; this is the deterministic destruction that defines CPython.
Why two different immortal thresholds
Py_INCREF short-circuits when ob_refcnt >= _Py_IMMORTAL_INITIAL_REFCNT (3<<30), but Py_DECREF short-circuits via _Py_IsImmortal, which (on 64-bit) tests the sign bit of the 32-bit count — equivalent to >= _Py_IMMORTAL_MINIMUM_REFCNT (1<<31 = 0x80000000). The decref threshold (1<<31) is lower than the incref threshold (3<<30), and the gap between them is roughly one billion. The refcount.h header comment states the rationale: the initial immortal value is set “near the middle of the range (2³¹, 2³²)” so “the refcount can be off by ~1 billion without affecting immortality,” and “to ensure that once an object becomes immortal, it remains immortal, the threshold for omitting increfs is much higher than for omitting decrefs” (refcount.h, 3.14.0). The practical hazard this guards against: a stable-ABI extension compiled against an older CPython lacks the immortality checks and does a plain ob_refcnt++/--. If it decrements an immortal object many times, the lower decref threshold means the count must fall a full billion before the object stops being recognized as immortal — wide self-healing headroom. The full treatment of immortal sentinels and de-immortalization risk is Immortal Objects; here the point is purely why the two macros test different bounds.
The immortality test, by build
_Py_IsImmortal has three forms (refcount.h, 3.14.0):
static inline Py_ALWAYS_INLINE int _Py_IsImmortal(PyObject *op)
{
#if defined(Py_GIL_DISABLED)
return (_Py_atomic_load_uint32_relaxed(&op->ob_ref_local) ==
_Py_IMMORTAL_REFCNT_LOCAL); // free-threaded: local == UINT32_MAX
#elif SIZEOF_VOID_P > 4
return _Py_CAST(PY_INT32_T, op->ob_refcnt) < 0; // 64-bit: sign bit of 32-bit count
#else
return op->ob_refcnt >= _Py_IMMORTAL_MINIMUM_REFCNT; // 32-bit platform: explicit compare
#endif
}The three branches correspond exactly to the three header layouts. On a 64-bit build the count is 32 bits, so reinterpreting it as a signed int32_t and asking “is it negative?” is the same as “is bit 31 set?” — a one-instruction test with no mask constant. On a real 32-bit platform the count is full-width Py_ssize_t, so the test is an explicit >= against _Py_IMMORTAL_MINIMUM_REFCNT (which is 1<<30 on 32-bit, not 1<<31). In the free-threaded build immortality is encoded as the thread-local sub-count being maxed to _Py_IMMORTAL_REFCNT_LOCAL (UINT32_MAX). These constants — _Py_IMMORTAL_INITIAL_REFCNT, _Py_IMMORTAL_MINIMUM_REFCNT, and their 32-bit and free-threaded variants — and the discrepancy with PEP 683’s originally-proposed single _Py_IMMORTAL_REFCNT/_Py_IMMORTAL_BIT scheme are detailed in Immortal Objects; the shipped source is authoritative over the PEP, which describes the original design (see the flag below).
PEP vs shipped source (verified 2026-06-01)
PEP 683 (the proposal) specified a single sentinel
_Py_IMMORTAL_REFCNT(top two bits set, near 2⁶²) detected by bitwise-AND with_Py_IMMORTAL_BIT. The shipped source uses a refined scheme — two thresholds (_Py_IMMORTAL_INITIAL_REFCNT = 3<<30,_Py_IMMORTAL_MINIMUM_REFCNT = 1<<31) with sign-bit detection on a 32-bit field, not a single sentinel near 2⁶². Re-verified againstrefcount.h@ v3.14.5. TreatInclude/refcount.hat the release tag as authoritative for current values; the PEP only for original rationale. Full detail in Immortal Objects.
At Zero — _Py_Dealloc and the Path to the Allocator
When a decrement lands on zero, Py_DECREF calls _Py_Dealloc(op) (Objects/object.c, 3.14.0):
void
_Py_Dealloc(PyObject *op)
{
PyTypeObject *type = Py_TYPE(op); // (1) the object's type
unsigned long gc_flag = type->tp_flags & Py_TPFLAGS_HAVE_GC;
destructor dealloc = type->tp_dealloc; // (2) the destructor slot
PyThreadState *tstate = _PyThreadState_GET();
intptr_t margin = _Py_RecursionLimit_GetMargin(tstate);
if (margin < 2 && gc_flag) { // (3) near stack limit + GC type?
_PyTrash_thread_deposit_object(tstate, (PyObject *)op); // -> defer via trashcan
return;
}
/* debug-build assertions elided */
(*dealloc)(op); // (4) call tp_dealloc
/* post-dealloc checks elided */
}Py_TYPE(op)readsob_type— the type object that defines this object’s behavior.tp_deallocis the function pointer in the type’s slot table that destroys an instance of this type. Every type that can reach refcount zero must supply one; theobject.hcontract notes it “should always be implemented except if … the reference count will never reach zero” (i.e. immortal/static types).- The trashcan. Before calling the destructor,
_Py_Deallocchecks the remaining C stack margin. Deallocation is recursive: a container’stp_deallocdecrefs its members, which may drop them to zero and recurse. A deeply nested structure (a million-deep linked list) could blow the C stack mid-destruction. The trashcan mechanism (_PyTrash_thread_deposit_object) detects when the stack margin is low and a GC-tracked container is being freed, and instead of recursing defers the object onto a per-thread list to be destroyed iteratively once the stack unwinds. This is why freeing a huge nested structure does not segfault. (The trashcan only engages for GC-tracked container types —gc_flag— since only those can chain deeply.) (*dealloc)(op)invokes the destructor. What it does depends on the type: for a container itPy_DECREFs each contained element (the cascade that can free a whole subtree, decrementing each child and recursively deallocating those that hit zero); for an object with a__del__finalizer it runs that Python code (so aPy_DECREFcan re-enter the interpreter — see Finalizers and the del Method); and finally it returns the object’s own bytes to the allocator domain it came from — typicallyPyObject_Freeback to a pymalloc pool, or onto a free list for hot types like tuples and frames. This is the join between the reclamation axis and the allocation stack mapped in CPython Memory Management Overview.
A Py_DECREF to zero therefore is not a single field write — it can trigger arbitrary __del__ Python code, a recursive cascade through an object graph, and a return of memory to pymalloc, all synchronously inside the decref. That is the full weight of “the object is destroyed now.”
Py_REFCNT and Py_SET_REFCNT
Reading and forcibly setting the count go through accessor inlines so that the 3.14 repack and the free-threaded split stay encapsulated. Py_REFCNT(ob) returns the count; on the default build it simply reads ob->ob_refcnt, but on the free-threaded build it must combine the two sub-counts (refcount.h, 3.14.0):
static inline Py_ssize_t _Py_REFCNT(PyObject *ob) {
#if !defined(Py_GIL_DISABLED)
return ob->ob_refcnt;
#else
uint32_t local = _Py_atomic_load_uint32_relaxed(&ob->ob_ref_local);
if (local == _Py_IMMORTAL_REFCNT_LOCAL) {
return _Py_IMMORTAL_INITIAL_REFCNT; // immortal: report the sentinel
}
Py_ssize_t shared = _Py_atomic_load_ssize_relaxed(&ob->ob_ref_shared);
return _Py_STATIC_CAST(Py_ssize_t, local) +
Py_ARITHMETIC_RIGHT_SHIFT(Py_ssize_t, shared, _Py_REF_SHARED_SHIFT);
#endif
}The free-threaded count is local + (shared >> _Py_REF_SHARED_SHIFT) — the owner thread’s fast local count plus the right-shifted shared count (the low bits of ob_ref_shared are state flags, so it is shifted to recover the actual shared count). Py_SET_REFCNT(ob, n) is the inverse, and it first checks _Py_IsImmortal and refuses to overwrite an immortal object’s count, then on 64-bit stores the low 32 bits (refcount.h, 3.14.0):
if (_Py_IsImmortal(ob)) {
return; // never de-immortalize via SET_REFCNT
}
#if SIZEOF_VOID_P > 4
ob->ob_refcnt = (PY_UINT32_T)refcnt; // 64-bit: write low 32 bits
#else
ob->ob_refcnt = refcnt; // 32-bit platform: full width
#endifPy_REFCNT is what sys.getrefcount reports (plus one for the temporary argument reference). For an immortal object it returns the inflated sentinel, which is why the C-API docs warn the value is meaningful only as 0, 1, or “in use” (C-API: refcounting).
Divergence Under Free-Threading — Biased and Deferred Counting
Everything above assumes the GIL serializes refcount mutation, which is what lets Py_INCREF/Py_DECREF be non-atomic. The free-threaded build (PEP 703) removes the GIL, so two threads can mutate the same object’s count concurrently — a plain ++/-- would lose updates and cause use-after-free. CPython’s answer is biased reference counting: each object’s count is split (as the free-threaded header above shows) into ob_ref_local, a fast, non-atomic count usable only by the object’s owning thread (ob_tid), and ob_ref_shared, an atomic count for every other thread (PEP 703). The common case — an object touched only by the thread that created it — pays no atomic; cross-thread references pay atomics on the shared count.
The mechanics this note can show concretely are the merge path in object.c. When the owner thread’s local count is exhausted, the two counts must be reconciled (Objects/object.c, 3.14.0):
void
_Py_MergeZeroLocalRefcount(PyObject *op)
{
assert(op->ob_ref_local == 0); // owner's local count hit zero
Py_ssize_t shared = _Py_atomic_load_ssize_acquire(&op->ob_ref_shared);
if (shared == 0) {
_Py_Dealloc(op); // no other thread holds a ref -> free
return;
}
/* otherwise atomically fold local into shared and continue */
}The key insight: the object is freed only when both the local count is zero and the merged shared count is zero — _Py_DecRefShared handles decrements from non-owner threads, and only the final reconciliation calls _Py_Dealloc. So the zero trigger still ends at the same _Py_Dealloc/tp_dealloc path described above; what changes is the bookkeeping that decides when zero is reached. Deferred reference counting is a separate technique (not the same as biased counting) that the free-threaded build also uses for a few object kinds whose references are too hot to count on every access; it is covered in Deferred Reference Counting. Both schemes, and the full free-threaded refcount design, belong to §8 of the Python Internals MOC — this section only shows where the single-threaded mechanics diverge and that they re-converge at _Py_Dealloc.
Failure Modes and Diagnostics
-
Negative refcount. A double-decref (releasing a reference twice) can drive
ob_refcntbelow zero or, more often, to zero on a still-referenced object, freeing it under another holder’s feet — a use-after-free. Debug builds catch the negative case via_Py_NegativeRefcount, which calls_PyObject_AssertFailedwith “object has negative ref count” and the file/line of the offending decref (Objects/object.c, 3.14.0). This is the single most common C-extension bug class; see Reference Counting in C Extensions. -
The dealloc cascade re-enters the interpreter. Because
tp_dealloccan run__del__, aPy_DECREFis a potential re-entry point. An object reachable from a global must be in a consistent state before the decref that might free it, which is whyPy_CLEAR(decref after nulling the field) exists. -
Stack-deep destruction without the trashcan would crash. The trashcan in
_Py_Deallocis what prevents a million-deep linked list from overflowing the C stack during recursive deallocation; without it, freeing such a structure segfaults. -
sys.getrefcountis off by one and meaningless for immortals. It reportsPy_REFCNT + 1(the argument’s temporary reference), and for immortal objects returns the pinned sentinel (e.g.3221225472on 64-bit GIL builds), not a real count. -
Reading the count tells you nothing about cycles. A positive
ob_refcntdoes not mean the object is reachable — it may be stranded in a cycle that only the cyclic GC can detect. Refcount mechanics handle acyclic reclamation; the cycle case is structurally invisible to them.
See Also
- Reference Counting — the object-model overview: the concept, deterministic destruction, the GIL connection. (Read first for the why.)
- Reference Counting in C Extensions — the C-API discipline: borrowed vs owned references extension authors must follow.
- PyObject and the Object Header — the full header layout
ob_refcntlives in. - Immortal Objects — the sentinel values, the PEP 683 discrepancy, de-immortalization risk.
- CPython Memory Management Overview — how the dealloc path returns bytes to the allocator stack (§6 orientation).
- The pymalloc Allocator / Free Lists — where freed object bytes go.
- The Cyclic Garbage Collector — reclaims the cycles refcount mechanics cannot.
- Finalizers and the del Method —
__del__running insidetp_dealloc. - Biased Reference Counting / Deferred Reference Counting — the free-threaded refcount schemes (§8).
- The Global Interpreter Lock — why the default-build mutations are non-atomic.
- Python Internals MOC — §6 “Memory Management.”