Python's weakref.ref on Bound Methods: The Subscriber That Unsubscribes Before Its First Event

2026-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.

The Bug

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.

The Fix

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).

Key Takeaway: 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.

All newsletters