How CPython Implements Weak References (weakref) in C
weakref.ReferenceType/ProxyType/CallableProxyType as PyWeakReference: the stealth (non-owning) pointer wr_object, wr_callback, the doubly-linked per-object weakref list via tp_weaklistoffset, basic-ref reuse, clearing on death, callback invocation, managed weakref slots for heap types, proxy forwarding, and free-threaded locking, traced through CPython 3.15.
Python’s weak references – weakref.ref, weakref.proxy, the machinery behind
weakref.WeakValueDictionary and __weakref__ – are implemented in CPython in
Objects/weakrefobject.c as a single C struct, PyWeakReference, that is reused
for three Python types: ReferenceType, ProxyType, and CallableProxyType.
A weak reference is a “stealth” pointer: it points at an object without
incrementing that object’s reference count, and when the object dies every weak
reference to it is cleared (and any callbacks run). This article traces that
implementation through the 3.15 source.
The object: PyWeakReference
The layout is defined in Include/cpython/weakrefobject.h:
// Include/cpython/weakrefobject.h
struct _PyWeakReference {
PyObject_HEAD
/* The object to which this is a weak reference, or Py_None if none.
* Note that this is a stealth reference: wr_object's refcount is
* not incremented to reflect this pointer. */
PyObject *wr_object;
/* A callable to invoke when wr_object dies, or NULL if none. */
PyObject *wr_callback;
/* A cache for wr_object's hash code. -1 if unknown. */
Py_hash_t hash;
/* Doubly-linked list pointers into the referenced object's weakref list. */
PyWeakReference *wr_prev;
PyWeakReference *wr_next;
vectorcallfunc vectorcall;
#ifdef Py_GIL_DISABLED
PyMutex *weakrefs_lock;
#endif
};Invariants:
wr_objectis either a live object orPy_None.Py_Noneis the sentinel for “dead”; it is not a real reference, so noPy_DECREFis owed for it.wr_objectis a stealth reference: its refcount is never incremented to account for this pointer. This is the entire point of a weak reference.wr_callbackis an owned (refcounted) reference, orNULL.- The object is GC-tracked (
Py_TPFLAGS_HAVE_GC) because it ownswr_callback(and, transitively, may keep a dead ref’s callback alive). - All three Python types (
ReferenceType,ProxyType,CallableProxyType) share this same struct; only theirtp_*slots differ. PyWeakref_CheckRef,PyWeakref_CheckProxy,PyWeakref_Checkare the type tests.PyWeakref_CheckRefExactis an exact-type test used for the “basic ref” fast paths.
The per-object weakref list
Every object that supports weak references carries a doubly-linked,
NULL-terminated list of the weak references pointing at it. The head of that
list is stored in the object itself, at an offset given by the type’s
tp_weaklistoffset:
// Include/internal/pycore_object.h
static inline PyObject **
_PyObject_GET_WEAKREFS_LISTPTR(PyObject *op)
{
Py_ssize_t offset = Py_TYPE(op)->tp_weaklistoffset;
return (PyObject **)((char *)op + offset);
}Support is gated by _PyType_SUPPORTS_WEAKREFS(type) which simply checks
type->tp_weaklistoffset != 0. Concrete examples in the source:
// Objects/funcobject.c
.tp_weaklistoffset = offsetof(PyFunctionObject, func_weakreflist),
// Objects/genobject.c
.tp_weaklistoffset = offsetof(PyGenObject, gi_weakreflist),
// Objects/moduleobject.c
.tp_weaklistoffset = offsetof(PyModuleObject, md_weaklist),
// Objects/setobject.c
.tp_weaklistoffset = offsetof(PySetObject, weakreflist),For heap (user-defined) classes, __weakref__ is added automatically unless
__slots__ forbids it. In typeobject.c this is recorded as a managed
weakref slot:
// Objects/typeobject.c
if (ctx->add_weak) {
type_add_flags(type, Py_TPFLAGS_MANAGED_WEAKREF);
type->tp_weaklistoffset = MANAGED_WEAKREF_OFFSET; // negative sentinel
}MANAGED_WEAKREF_OFFSET is a negative offset (-2 or -4 sizeof(PyObject*),
depending on the build) into the managed pointer area just before the instance
dict, so the head pointer lives in the object’s pre-header region rather than
in a named field. subtype_getweakref exposes this as the __weakref__
attribute and returns the head weakref (or None):
// Objects/typeobject.c
static PyObject *
subtype_getweakref(PyObject *obj, void *context)
{
...
weaklistptr = (PyObject **)((char *)obj + type->tp_weaklistoffset);
if (*weaklistptr == NULL) {
result = Py_NewRef(Py_None);
}
else if (_Py_TryIncref(*weaklistptr)) {
result = *weaklistptr;
}
else {
result = Py_NewRef(Py_None);
}
...
}Construction: get_or_create_weakref
The public entry points are PyWeakref_NewRef and PyWeakref_NewProxy:
// Objects/weakrefobject.c
PyObject *
PyWeakref_NewRef(PyObject *ob, PyObject *callback)
{
return (PyObject *)get_or_create_weakref(&_PyWeakref_RefType, ob, callback);
}
PyObject *
PyWeakref_NewProxy(PyObject *ob, PyObject *callback)
{
PyTypeObject *type = &_PyWeakref_ProxyType;
if (PyCallable_Check(ob)) {
type = &_PyWeakref_CallableProxyType;
}
return (PyObject *)get_or_create_weakref(type, ob, callback);
}get_or_create_weakref does the real work. First it validates that the target
supports weak references, normalizes callback == Py_None to NULL, then:
// Objects/weakrefobject.c
PyWeakReference **list = GET_WEAKREFS_LISTPTR(obj);
if ((type == &_PyWeakref_RefType) ||
(type == &_PyWeakref_ProxyType) ||
(type == &_PyWeakref_CallableProxyType))
{
LOCK_WEAKREFS(obj);
PyWeakReference *basic_ref = try_reuse_basic_ref(*list, type, callback);
if (basic_ref != NULL) {
UNLOCK_WEAKREFS(obj);
return basic_ref; // reused an existing callback-less ref
}
PyWeakReference *newref = allocate_weakref(type, obj, callback);
if (newref == NULL) { UNLOCK_WEAKREFS(obj); return NULL; }
insert_weakref(newref, list);
UNLOCK_WEAKREFS(obj);
return newref;
}
else {
// Subtypes / custom types: allocate outside the lock
PyWeakReference *newref = allocate_weakref(type, obj, callback);
if (newref == NULL) return NULL;
LOCK_WEAKREFS(obj);
insert_weakref(newref, list);
UNLOCK_WEAKREFS(obj);
return newref;
}Two important optimizations:
Basic-ref reuse. A weak reference with no callback of the exact basic type (
weakref.ref/weakref.proxy) is “canonical”: at most one of each kind (ref and proxy) is kept at the head of the list.try_reuse_basic_refreturns an existing one instead of allocating a new object, incrementing its refcount. This is whyweakref.ref(obj) is weakref.ref(obj)can be true for callback-less refs.Lock discipline. For the three builtin types allocation happens inside the lock (cheap,
tp_alloc); for subtypes, allocation happens outside the lock because a subtype’stp_allocmay run arbitrary Python code.
init_weakref stores the stealth pointer and owns the callback:
// Objects/weakrefobject.c
static void
init_weakref(PyWeakReference *self, PyObject *ob, PyObject *callback)
{
self->hash = -1;
self->wr_object = ob; // stealth: no incref
self->wr_prev = NULL;
self->wr_next = NULL;
self->wr_callback = Py_XNewRef(callback);
self->vectorcall = weakref_vectorcall;
#ifdef Py_GIL_DISABLED
self->weakrefs_lock = &WEAKREF_LIST_LOCK(ob);
_PyObject_SetMaybeWeakref(ob);
_PyObject_SetMaybeWeakref((PyObject *)self);
#endif
}allocate_weakref uses type->tp_alloc(type, 0) (so subtypes allocate
correctly) and then init_weakref.
Insertion ordering
The list is kept in a canonical order so the basic (callback-less) refs sit at the head:
// Objects/weakrefobject.c
static void
insert_weakref(PyWeakReference *newref, PyWeakReference **list)
{
PyWeakReference *ref, *proxy;
get_basic_refs(*list, &ref, &proxy);
PyWeakReference *prev;
if (is_basic_ref(newref)) {
prev = NULL; // head
}
else if (is_basic_proxy(newref)) {
prev = ref; // after the basic ref
}
else {
prev = (proxy == NULL) ? ref : proxy; // after both basic refs
}
if (prev == NULL) insert_head(newref, list);
else insert_after(newref, prev);
}insert_head/insert_after splice the node into the doubly-linked list and
update wr_prev/wr_next. The first node’s wr_prev stays NULL and acts
as the list head stored in the object.
Reading a weak reference
The core accessor is _PyWeakref_GET_REF (inlined in
Include/internal/pycore_weakref.h):
// Include/internal/pycore_weakref.h
static inline PyObject* _PyWeakref_GET_REF(PyObject *ref_obj)
{
PyWeakReference *ref = _Py_CAST(PyWeakReference*, ref_obj);
PyObject *obj = FT_ATOMIC_LOAD_PTR(ref->wr_object);
if (obj == Py_None) {
return NULL; // cleared
}
LOCK_WEAKREFS(obj);
#ifdef Py_GIL_DISABLED
if (ref->wr_object == Py_None) { // re-check under lock
UNLOCK_WEAKREFS(obj);
return NULL;
}
#endif
if (_Py_TryIncref(obj)) { // stealth -> real ref, if alive
UNLOCK_WEAKREFS(obj);
return obj; // new reference (caller owns)
}
UNLOCK_WEAKREFS(obj);
return NULL;
}Because wr_object is stealth, reading it requires trying to incref the
target under the lock: if the target is already dead (refcount == 0), the
incref fails and NULL is returned. On success the caller receives a new
owned reference.
The public API wraps this:
// Objects/weakrefobject.c
int
PyWeakref_GetRef(PyObject *ref, PyObject **pobj)
{
...
*pobj = _PyWeakref_GET_REF(ref);
return (*pobj != NULL); // 1 if live, 0 if dead
}PyWeakref_IsDead / _PyWeakref_IS_DEAD additionally handle the GC/trashcan
delay case: a target whose refcount hit zero but whose weakrefs have not yet
been cleared is still reported as dead via _is_dead().
The ref object itself is callable (via vectorcall), so ref() returns the
target or None:
// Objects/weakrefobject.c
static PyObject *
weakref_vectorcall(PyObject *self, ...)
{
PyObject *obj = _PyWeakref_GET_REF(self);
if (obj == NULL) {
Py_RETURN_NONE;
}
return obj; // new reference
}Hashing and equality
weakref_hashcaches the target’s hash inwr_hashunder the weakref’s own critical section; a dead ref raisesTypeError: weak object has gone away.weakref_richcomparecompares targets: if either is dead, equality reduces to identity of the two ref objects; otherwise it delegates toPyObject_RichCompareon the targets.
Clearing: when the target dies
The central routine is clear_weakref_lock_held, which removes a node from the
target’s list and flips wr_object to Py_None:
// Objects/weakrefobject.c
static void
clear_weakref_lock_held(PyWeakReference *self, PyObject **callback)
{
if (self->wr_object != Py_None) {
PyWeakReference **list = GET_WEAKREFS_LISTPTR(self->wr_object);
if (*list == self) {
FT_ATOMIC_STORE_PTR(*list, self->wr_next);
}
FT_ATOMIC_STORE_PTR(self->wr_object, Py_None);
if (self->wr_prev != NULL) self->wr_prev->wr_next = self->wr_next;
if (self->wr_next != NULL) self->wr_next->wr_prev = self->wr_prev;
self->wr_prev = NULL;
self->wr_next = NULL;
}
if (callback != NULL) {
*callback = self->wr_callback; // steal
self->wr_callback = NULL;
}
}Note that the callback is stolen, not invoked, here. Clearing happens under
the weakref list lock, and invoking arbitrary Python code under that lock is
forbidden (especially in free-threaded builds where the lock is
_Py_LOCK_DONT_DETACH). So clear_weakref_lock_held only detaches the
callback; the caller decides when to run it.
There are three callers:
PyObject_ClearWeakRefs(object)– called fromtp_deallocwhen the target’s refcount hits zero. It clears every weakref and runs callbacks in two phases:- First, the callback-less basic ref/proxy at the head of the list are cleared under the lock (no callbacks to run).
- Then each remaining ref is cleared under the lock, its callback stolen,
and the (ref, callback) pair stored in a tuple. After the lock is released,
callbacks are invoked via
handle_callback:cstatic void handle_callback(PyWeakReference *ref, PyObject *callback) { PyObject *cbresult = PyObject_CallOneArg(callback, (PyObject *)ref); if (cbresult == NULL) { PyErr_FormatUnraisable("Exception ignored while " "calling weakref callback %R", callback); } else { Py_DECREF(cbresult); } } - The original exception (if any) is saved and restored around this, so a failing callback does not clobber an in-flight exception.
GC (
gc_clearinweakrefobject.cand the inlined copy ingcmodule.c’shandle_weakrefs). During collection, weakrefs are cleared but callbacks are deliberately left intact and uncalled, because running Python code mid-GC is unsafe._PyWeakref_ClearRef(note: capital R) is the GC-facing variant that clears the ref while leaving the callback fortp_deallocto decref later.Deallocation of the weakref itself (
weakref_dealloc/proxy_dealloc) callsclear_weakref(which locks and thenPy_XDECREFs the stolen callback) and thentp_free.
_PyWeakref_ClearWeakRefsNoCallbacks and _PyStaticType_ClearWeakRefs are
further variants used for bulk clearing without callbacks (e.g. finalization
of static types).
The callback member
wr_callback is exposed read-only to Python as __callback__:
// Objects/weakrefobject.c
static PyMemberDef weakref_members[] = {
{"__callback__", _Py_T_OBJECT,
offsetof(PyWeakReference, wr_callback), Py_READONLY},
{NULL}
};weakref.ref.__init__ is a no-op (the real construction happens in
__new__), so ref(obj, callback) creates a new weakref with a callback
that cannot be reused via the basic-ref fast path (callbacks force a fresh
node).
Proxy objects
ProxyType and CallableProxyType are the same PyWeakReference struct, but
instead of being callable to retrieve the target, they forward every
operation to the target. The implementation is a set of macros that
unwrap the proxy into the real object, call the generic C API, and decref:
// Objects/weakrefobject.c
#define UNWRAP(o) \
if (PyWeakref_CheckProxy(o)) { \
o = _PyWeakref_GET_REF(o); \
if (!proxy_check_ref(o)) { return NULL; } // ReferenceError if dead \
} else { \
Py_INCREF(o); \
}
#define WRAP_UNARY(method, generic) \
static PyObject * method(PyObject *proxy) { \
UNWRAP(proxy); \
PyObject* res = generic(proxy); \
Py_DECREF(proxy); \
return res; \
}
#define WRAP_BINARY(method, generic) \
static PyObject * method(PyObject *x, PyObject *y) { \
UNWRAP(x); UNWRAP(y); \
PyObject* res = generic(x, y); \
Py_DECREF(x); Py_DECREF(y); \
return res; \
}proxy_check_ref raises ReferenceError: weakly-referenced object no longer exists when the target is gone. These macros generate the number slots
(proxy_add, proxy_sub, …), sequence/mapping slots (proxy_length,
proxy_getitem, proxy_setitem, proxy_contains), iterator slots
(proxy_iter, proxy_iternext), and attribute access (proxy_getattr,
proxy_setattr). CallableProxyType additionally sets
tp_call = proxy_call, which unwraps and calls the target.
Notable details:
- Proxies have no
tp_hash(intentionally omitted) because they are “mutable” once the reference dies. proxy_bool,proxy_repr, etc. fetch the target via_PyWeakref_GET_REFdirectly rather than throughUNWRAP.proxy_deallocis the same asweakref_dealloc(clear_weakref+PyObject_GC_Del).
Free-threading
In the no-GIL build, weakref state is protected by striped locks keyed on the
target’s address (WEAKREF_LIST_LOCK(obj)), acquired with
_Py_LOCK_DONT_DETACH so that weakref clearing can run during operations like
WeakValueDictionary deletion that must not suspend. The weakref’s own
weakrefs_lock pointer is captured at creation (so a ref can be locked even
after its target has been set to Py_None). _PyWeakref_GET_REF and
_PyWeakref_IS_DEAD take and release these locks; _PyObject_SetMaybeWeakref
marks both the target and the ref itself so the runtime knows a weakref may
exist. The GC is exempt from locking because the world is stopped during
collection.
Summary of the data flow
weakref.ref(obj, callback)
-> get_or_create_weakref(RefType, obj, callback)
-> supports weakrefs? (tp_weaklistoffset != 0)
-> list = GET_WEAKREFS_LISTPTR(obj) // head pointer in obj
-> reuse basic ref if callback is None and one exists
-> allocate_weakref -> tp_alloc + init_weakref (stealth wr_object,
owned wr_callback)
-> insert_weakref into doubly-linked list (basic refs at head)
-> return ref
ref() -> _PyWeakref_GET_REF -> try incref target under lock -> new ref or None
target dies (refcount 0) -> tp_dealloc -> PyObject_ClearWeakRefs(target)
-> clear each node (wr_object = Py_None, unlink)
-> steal callbacks, invoke them outside the lock
-> ref() now returns None; ref.__callback__ is NoneKey takeaways
weakref.ReferenceType,ProxyType, andCallableProxyTypeare allPyWeakReference; the only differences are theirtp_*slots.wr_objectis a stealth (non-owning) pointer;Py_Nonemarks death. Reading it requires_Py_TryIncrefunder the target’s lock.- Each weakref-able object stores the head of a doubly-linked list of weakrefs
at
tp_weaklistoffset(a real offset for builtin types, a managed negative sentinel for heap classes with__weakref__). - Callback-less basic refs (one ref + one proxy) are reused instead of reallocated; refs with callbacks are always new nodes.
- Clearing unlinks the node and steals the callback; callbacks run outside the lock (and are deferred entirely during GC).
- Proxies forward every operation by unwrapping to the target via
UNWRAP, raisingReferenceErrorwhen the target is gone. - Free-threaded builds protect the list with striped, non-detaching locks keyed on the target’s address.
Further reading
Objects/weakrefobject.c–PyWeakReference,get_or_create_weakref,clear_weakref_lock_held,PyObject_ClearWeakRefs,handle_callback, proxy macros and slots.Include/cpython/weakrefobject.h– struct layout.Include/internal/pycore_weakref.h–_PyWeakref_GET_REF,_PyWeakref_IS_DEAD, locking macros.Include/internal/pycore_object.h–_PyObject_GET_WEAKREFS_LISTPTR,_PyType_SUPPORTS_WEAKREFS, managed offsets.Objects/typeobject.c–tp_weaklistoffset,subtype_getweakref, managed weakref slot allocation.Modules/gcmodule.c–handle_weakrefs(inlined clearing variant).Lib/weakref.py– Python-levelWeakValueDictionary,WeakSet,finalize.