weakref.ref on Bound Methods: The Subscriber That Unsubscribes Before Its First Event2026-09-06
This Publisher holds weak references to callbacks so subscribers don't leak when they go out of scope. It works beautifully with plain functions — and vanishes silently when you subscribe a method.
import weakref
class Publisher:
def __init__(self):
self.subs = []
def subscribe(self, callback):
self.subs.append(weakref.ref(callback))
def publish(self, event):
for ref in self.subs:
cb = ref()
if cb is not None:
cb(event)
class Handler:
def on_event(self, event):
print(f"got: {event}")
pub = Publisher()
h = Handler() # strong reference to the instance
pub.subscribe(h.on_event)
pub.publish("ping") # prints nothing. The subscriber is already gone.
In Python, methods aren't stored on instances. When you write h.on_event, the descriptor protocol constructs a brand-new bound method object that wraps h and the underlying function. Access the attribute twice and you get two different objects:
>>> h.on_event is h.on_event
False
So when subscribe runs weakref.ref(callback), the callback it sees is a freshly-minted bound method with exactly one reference: the parameter callback. As soon as subscribe returns, that reference disappears, the bound method is collected, and the weakref becomes dead. The instance h is still alive and well — but the *thing you held a weakref to* was the transient wrapper, not the instance.
The trap is nastier than it looks because the code appears to work with equivalent-looking inputs. Subscribing a plain function or a lambda kept alive by the caller works fine. Subscribing h.on_event or self.on_event from inside a constructor silently drops the callback the moment control returns. Tests that keep the bound method in a local variable will pass; production code that inlines the expression will fail.
Use weakref.WeakMethod, which exists precisely for this case. It stores a weak reference to the instance and a strong reference to the underlying function (functions live as long as their class, so that's fine), and reconstructs the bound method when you call the ref:
import weakref
from types import MethodType
class Publisher:
def __init__(self):
self.subs = []
def subscribe(self, callback):
if isinstance(callback, MethodType):
self.subs.append(weakref.WeakMethod(callback))
else:
self.subs.append(weakref.ref(callback))
def publish(self, event):
for ref in self.subs:
cb = ref()
if cb is not None:
cb(event)
Now pub.publish("ping") prints got: ping, and the subscription is properly dropped only when h itself is collected.
The same trap bites anywhere you take a weak reference to something Python synthesizes on demand: bound methods, partial objects created inline, and — less commonly — closures constructed at the call site. The rule of thumb: if x is x can be False, don't weakref.ref(x).
obj.method creates a fresh bound method on every access, so weakref.ref(obj.method) holds a reference to a wrapper that dies the instant your function returns — reach for weakref.WeakMethod instead.
