Skip to main content

Objects/weakrefobject.c: Weak Reference Linked List and Proxy Dispatch

Objects/weakrefobject.c implements CPython's weak reference machinery. A weak reference does not prevent garbage collection of its referent. When the referent is finalized the GC clears every PyWeakReference node in the referent's intrusive linked list, sets wr_object to Py_None, and fires any registered callbacks. Proxy objects layer transparent attribute forwarding on top of the same clearing infrastructure.

Map

LinesSymbolRole
1-60PyWeakReference struct helperswr_object, wr_callback, hash, doubly-linked wr_object_prev / wr_object_next
61-150insert_head, insert_afterSplice a new node into the per-referent list held at ob_weaklist
151-250_PyWeakref_GetWeakrefCountWalk the list; return the number of live refs for a given referent
251-370_PyWeakref_ClearRefCalled by GC deallocation; zeroes wr_object, fires wr_callback, unlinks node
371-490PyWeakref_NewRef, PyWeakref_NewProxyConstructors; deduplicate callback-less refs against the list head
491-580PyWeakref_GetObject, PyWeakref_GET_REFSafe and fast referent accessors; 3.14 added GET_REF returning a strong reference
581-720Proxy tp_getattro, tp_setattroForward attribute access to the referent; raise ReferenceError when cleared
721-850Proxy numeric / sequence slotsTransparent operator forwarding so a proxy behaves identically to the referent
851-900PyWeakref_Type, PyWeakProxy_TypeType object registration; tp_hash set for callback-less refs only

Reading

Intrusive linked list on ob_weaklist

The referent does not hold a Python-level list. Instead its memory block contains a single pointer at tp_weaklistoffset bytes past the object head. That pointer is the head of a C doubly-linked list of PyWeakReference nodes. Every new ref splices itself in; the GC reads the head pointer during finalization to find all refs without an extra allocation.

// CPython: Objects/weakrefobject.c:62 insert_head
static void
insert_head(PyWeakReference *newref, PyWeakReference **list)
{
PyWeakReference *next = *list;
newref->wr_object_prev = NULL;
newref->wr_object_next = next;
if (next != NULL)
next->wr_object_prev = newref;
*list = newref;
}

PyWeakref_NewRef deduplicates: if the referent already has a callback-less ref at the list head it returns that existing ref with an incremented refcount rather than allocating a new node. This makes weakref.ref(x) is weakref.ref(x) true for any x that has not been collected.

_PyWeakref_ClearRef during deallocation

The GC calls PyObject_ClearWeakRefs (in Objects/object.c) on every object with a non-NULL weaklist pointer before invoking tp_dealloc. That function iterates the list and calls _PyWeakref_ClearRef on each node.

// CPython: Objects/weakrefobject.c:270 _PyWeakref_ClearRef
void
_PyWeakref_ClearRef(PyWeakReference *self)
{
PyObject *callback = self->wr_callback;
self->wr_callback = NULL;

/* Replace the referent pointer with Py_None so GET_OBJECT
callers get None rather than a dangling pointer. */
PyObject *referent = self->wr_object;
self->wr_object = Py_NewRef(Py_None);
Py_DECREF(referent);

/* Unlink from the per-referent list. */
if (self->wr_object_prev != NULL)
self->wr_object_prev->wr_object_next = self->wr_object_next;
if (self->wr_object_next != NULL)
self->wr_object_next->wr_object_prev = self->wr_object_prev;

if (callback != NULL) {
PyObject *res = PyObject_CallOneArg(callback, (PyObject *)self);
Py_XDECREF(res);
Py_DECREF(callback);
}
}

The list walk in PyObject_ClearWeakRefs takes a snapshot of the next pointer before calling _PyWeakref_ClearRef so that a callback that itself creates or destroys refs does not corrupt the iteration.

PyWeakref_GetObject vs PyWeakref_GET_REF (3.14)

Before 3.14 the only safe public accessor was PyWeakref_GetObject, which returns a borrowed reference to Py_None when cleared. Because borrowed refs race with refcount-zero objects in free-threaded builds, 3.14 added PyWeakref_GET_REF which returns a strong (owning) reference or writes NULL to the output pointer.

// CPython: Objects/weakrefobject.c:510 PyWeakref_GET_REF (3.14)
int
PyWeakref_GET_REF(PyObject *ref, PyObject **pobj)
{
PyWeakReference *self = (PyWeakReference *)ref;
PyObject *obj = self->wr_object;
if (obj == Py_None) {
*pobj = NULL;
return 0; /* referent is gone */
}
*pobj = Py_NewRef(obj);
return 1; /* caller owns the reference */
}

/* Legacy borrowed-ref form retained for ABI compatibility: */
PyObject *
PyWeakref_GetObject(PyObject *ref)
{
return ((PyWeakReference *)ref)->wr_object; /* may be Py_None */
}

In gopy objects/weakref.go exposes both shapes: (*WeakRef).GetObject() returns an Object (may be None) and (*WeakRef).GetRef() returns (Object, bool) where the bool is false when the referent is gone. The strong-ref form is preferred in all new VM code.

Proxy tp_getattro dispatch

A proxy forwards every attribute access to the live referent. If the referent has been cleared the proxy raises ReferenceError. The check-and-forward pattern is replicated for every slot (numeric operators, sequence methods, etc.) so a proxy is indistinguishable from the referent for runtime dispatch.

// CPython: Objects/weakrefobject.c:594 proxy_getattr
static PyObject *
proxy_getattr(PyWeakReference *proxy, PyObject *name)
{
if (!proxy_check_ref((PyObject *)proxy))
return NULL; /* ReferenceError already set */
return PyObject_GetAttr(proxy->wr_object, name);
}

// CPython: Objects/weakrefobject.c:581 proxy_check_ref
static int
proxy_check_ref(PyObject *proxy)
{
if (((PyWeakReference *)proxy)->wr_object == Py_None) {
PyErr_SetString(PyExc_ReferenceError,
"weakly-referenced object no longer exists");
return 0;
}
return 1;
}

gopy notes

objects/weakref.go ports PyWeakReference as WeakRef (callback-less) and WeakRefWithCallback (with wr_callback). The linked list uses Go pointer fields prev *WeakRef and next *WeakRef; the referent carries a weakListHead *WeakRef field populated when tp_weaklistoffset is non-zero.

_PyWeakref_ClearRef maps to (*WeakRef).Clear(). PyObject_ClearWeakRefs maps to the ClearWeakRefs(obj Object) function that walks the list head obtained from obj.Type().WrListOffset.

The deduplication logic in PyWeakref_NewRef is preserved: gopy's NewWeakRef(obj, nil) checks the existing list head before allocating.

PyWeakref_GET_REF maps to (*WeakRef).GetRef() (Object, bool). The old PyWeakref_GetObject borrowed-ref form maps to (*WeakRef).GetObject() and is kept for internal use only; public VM code calls GetRef.

Proxy slots are implemented via a WeakProxy struct in the same file. tp_getattro maps to (*WeakProxy).GetAttr which calls proxy_check_ref then delegates to GetAttr(wr.GetObject(), name).

CPython 3.14 renamed the list pointers from wr_prev/wr_next to wr_object_prev/wr_object_next. gopy uses prev/next internally (the public name does not appear in the Go API) so no rename is needed.