weakref.finalize(obj, obj.method) Silently Keeps obj Alive Forever (tested)

weakref.finalize(obj, callback, *args) is meant to run callback once obj becomes unreachable -- a safer alternative to __del__. It works by holding a weak reference to obj, but a strong reference to callback and every argument in *args. Passing obj.some_method as that callback -- a natural-looking way to register "call this object's own cleanup method" -- passes a bound method, and a bound method holds a strong reference to obj internally. We measured exactly what that does.

1. A bound-method callback keeps the object alive -- the finalizer never fires early

import weakref, gc

class Resource:
    def __init__(self, name):
        self.name = name
        self._finalizer = weakref.finalize(self, self.cleanup, self.name)

    def cleanup(self, name):
        print(f"cleaning up {name}")

r = Resource("r1")
ref = weakref.ref(r)
del r
gc.collect()
print("still alive?", ref() is not None)
still alive? True

Measured: after deleting the only other reference to r and forcing a collection, the object is still alive -- ref() is not None. No exception, no warning. self.cleanup is a bound method, and a bound method keeps a strong reference to the instance it's bound to (self.cleanup.__self__ is self). weakref.finalize keeps a strong reference to the callback it was given, so it ends up holding the object alive through its own cleanup hook -- defeating the entire point of registering it.

2. The "cleanup" does eventually run -- just not when you think, and not because of anything you did

# continuing straight from the same process, no further code --
# the only output that ever appears is at interpreter shutdown:
#   cleaning up r1

Measured: the print(f"cleaning up {name}") line from the first repro only ever printed once, at process exit, as part of weakref.finalize's own atexit-style shutdown pass -- not at del r, not at gc.collect(), and not at any point where the program could know it had happened. In a long-running process (a server, a worker loop), that means the resource leaks for the entire lifetime of the process, invisibly.

3. The fix: pass a plain function and plain data, not a bound method

import weakref, gc

def cleanup(name):
    print(f"cleaning up {name}")

class Resource:
    def __init__(self, name):
        self.name = name
        self._finalizer = weakref.finalize(self, cleanup, self.name)

r = Resource("r2")
ref = weakref.ref(r)
del r
gc.collect()
print("still alive?", ref() is not None)
cleaning up r2
still alive? False

Measured: with a module-level function and a plain string instead of a bound method, the same sequence (del then gc.collect()) collects the object immediately and the cleanup callback runs right away. The only change is removing every strong reference back to self from the arguments passed to finalize() -- the fix is data, not logic.

Frequently asked questions

Does weakref.finalize(obj, obj.method) raise an error if the callback keeps obj alive?

No -- we measured it: no exception, no warning, no ResourceWarning. The object is simply never collected while the process runs, and the cleanup callback doesn't fire until interpreter shutdown.

Why does passing a bound method to weakref.finalize keep the object alive?

We measured that a bound method (self.cleanup) holds a strong reference to self internally (self.cleanup.__self__ is self), and weakref.finalize keeps a strong reference to whatever callback it's given -- so the finalizer ends up strongly referencing the very object it's supposed to let get collected.

When does the finalizer callback eventually run if the object is leaking this way?

We measured it running exactly once, at process/interpreter shutdown via weakref.finalize's own exit handling -- not at any point tied to del or gc.collect() in the running program, which in a long-lived process means the resource effectively never gets cleaned up while it's running.

How do you fix weakref.finalize so it doesn't keep the object alive?

We measured that replacing the bound method with a plain function (or any callable that doesn't hold a reference back to the target object) and passing plain data as the arguments collects the object immediately on the next del + gc.collect(), with the callback firing right away.

Written with AI assistance. Measured on 2026-10-08 with Python 3.13.5's built-in weakref module: registered weakref.finalize(obj, obj.method, ...) using a bound method of the target object as the callback, deleted the only other reference, ran gc.collect(), and checked whether the object was actually collected -- then repeated with a plain function instead of a bound method. PlainHeaders is our extension; Python's weakref module is built in.