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_INCREF and Py_DECREF macros, how immortal objects are detected by those macros, and what actually happens at zero — _Py_Dealloc reaching through tp_dealloc to destroy the object and return its bytes to the allocator (CPython Include/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 union lets the same 64 bits be addressed two ways: as one PY_INT64_T ob_refcnt_full (used to initialize or copy all three sub-fields in a single store), or as the inner anonymous struct of three named fields. The anonymous struct means you write op->ob_refcnt directly, not op->u.s.ob_refcnt.
  • uint32_t ob_refcnt is 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_overflow and uint16_t ob_flags are the two new 16-bit fields that share the upper half of the word with… nothing the old layout used. Verified against refcount.h @ v3.14.5, ob_flags holds 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_REFCNT shifts those bits by << 48 into the ob_flags slot of the 64-bit word. ob_overflow is unused reserved padding — grepping object.h, refcount.h, and Objects/object.c at v3.14.5 finds only its declaration, never a read or write; it exists so the packed struct lines up with the 64-bit ob_refcnt_full union member. These fields existed nowhere in 3.13 — the old Py_ssize_t ob_refcnt used the whole word for the count.

Resolved (2026-06-01)

ob_flags bit enumeration and ob_overflow’s (non-)role verified against Include/refcount.h, Include/object.h, and Objects/object.c at v3.14.5. Only two object-level flags exist (_Py_IMMORTAL_FLAGS=1, _Py_STATICALLY_ALLOCATED_FLAG=4); these are distinct from the type-level Py_TPFLAGS_* in tp_flags. ob_overflow is 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
  1. Read ob_refcnt into 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.
  2. 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.
  3. _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.
  4. 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.
  5. The common path: store cur_refcnt + 1. A plain uint32_t add. (The actual refcount.h uses saturated arithmetic guarded so an immortal value cannot wrap, but for a mortal object well below 3<<30 this 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
}
  1. _Py_IsImmortal(op) is the immortality test (defined below). Note it is a different test from Py_INCREF’s threshold check — this is the deliberate asymmetry.
  2. Immortal → return without writing, exactly as incref.
  3. Pre-decrement --op->ob_refcnt and compare the result to zero. If still positive, the object stays alive and we are done.
  4. 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 against refcount.h @ v3.14.5. Treat Include/refcount.h at 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 */
}
  1. Py_TYPE(op) reads ob_type — the type object that defines this object’s behavior.
  2. tp_dealloc is 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; the object.h contract notes it “should always be implemented except if … the reference count will never reach zero” (i.e. immortal/static types).
  3. The trashcan. Before calling the destructor, _Py_Dealloc checks the remaining C stack margin. Deallocation is recursive: a container’s tp_dealloc decrefs 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.)
  4. (*dealloc)(op) invokes the destructor. What it does depends on the type: for a container it Py_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 a Py_DECREF can 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 — typically PyObject_Free back 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
#endif

Py_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_refcnt below 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_AssertFailed with “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_dealloc can run __del__, a Py_DECREF is 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 why Py_CLEAR (decref after nulling the field) exists.

  • Stack-deep destruction without the trashcan would crash. The trashcan in _Py_Dealloc is what prevents a million-deep linked list from overflowing the C stack during recursive deallocation; without it, freeing such a structure segfaults.

  • sys.getrefcount is off by one and meaningless for immortals. It reports Py_REFCNT + 1 (the argument’s temporary reference), and for immortal objects returns the pinned sentinel (e.g. 3221225472 on 64-bit GIL builds), not a real count.

  • Reading the count tells you nothing about cycles. A positive ob_refcnt does 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