skip to content

Why does a callback registry holding obj.method keep that instance alive in Python?

level: seniorimportance: should knowfreq 32%

answer

  1. obj.method is not the function you wrote
  2. The wrapper remembers something about the object
  3. Two dunder attributes on a bound method
  4. __self__ is a strong reference
  5. A new wrapper on every lookup

basics

~20 s

Looking up obj.method builds a bound method object holding the instance in self and the underlying function in func. That is an ordinary strong reference, so whatever stores the bound method keeps the instance alive.

solid answer

~40 s

`obj.method` is not the function you wrote; it is a fresh bound-method object whose `__self__` is `obj` and whose `__func__` is the plain function from the class `__dict__`. `__self__` is an ordinary strong reference, so a long-lived registry of callbacks pins every registered instance and everything it holds — a steady, cycle-free climb in resident memory for a service that rebuilds its objects on each reload. Two consequences follow. `obj.method is obj.method` is `False`, since a new wrapper is built per lookup, so identity-based unregistration silently misses while equality-based removal still works. And `Cls.method` is just the function, so the usual fixes are an explicit unregister, or holding the instance weakly and re-binding at dispatch time.

code

python · 13 lines
python
class FlagCache:
    def reload(self):
        return "reloaded"


obj = FlagCache()
m = obj.reload

print(m.__self__ is obj)                             # True  - the instance
print(m.__func__ is FlagCache.__dict__["reload"])    # True  - the raw function
print(obj.reload is obj.reload)                      # False - a new wrapper each lookup
print(obj.reload == obj.reload)                      # True  - but they compare equal
print(FlagCache.reload(obj))                         # reloaded - unbound call

go deeper

for a junior

Know that obj.method can be stored in a variable and called later, and that it remembers which object it came from. The two attributes to recall are func and self.

for a middle

Explain that attribute lookup builds a new bound-method wrapper each time, why identity comparison of two lookups is False while equality is True, and that Cls.method(obj) is the same call written the long way.

for a senior

Diagnose the production shape: steadily climbing memory with no cycle, traced to a long-lived registry of bound methods. Be able to confirm retention with a weak reference and a forced collection, and to argue for an explicit unregister over a clever weak-callback scheme.

for a principal

Own the design rule for callback ownership across services: who registers, who removes, and whether registries hold strongly by contract. Decide when weak callbacks are worth their debugging cost against simply making lifecycles explicit.

### What `obj.method` really is `obj.method` is not the function written in the class body. Attribute lookup builds a **bound method object**: a small wrapper carrying two attributes. `__func__` is the plain function taken from the class `__dict__`, and `__self__` is the object the call will pass as the first argument. Calling the wrapper is calling `__func__(__self__, *args)`. The wrapper is constructed on each lookup, by the protocol that turns a function stored on a class into a bound callable; the details of that protocol are a topic of their own, but its output — a fresh wrapper holding the instance — is what causes the behaviour below. `__self__` is an **ordinary strong reference**. Nothing about it is weak, and no special case exempts it from the reference counting that governs every other attribute. So a container that holds a bound method holds the instance behind it, transitively holding everything that instance references. ### The failure this produces A feature-flag service registers a callback per cache object — `registry.append(cache.reload)` — and drops the cache when the flag definitions are reloaded. Each reload builds fresh cache objects for the 6,800 flag rows and appends fresh callbacks. Nothing removes the old entries, so every generation of caches stays alive through `__self__`, along with the parsed flag data each one holds. Resident memory climbs in flat steps, one step per reload; there is no cycle to blame, no `__del__` doing anything strange, and the objects look perfectly ordinary in a heap dump apart from being referenced by a list of bound methods. The registry is the leak, and the bound method is how it holds on. Anything that stores callables has this shape: signal and event registries, retry and scheduler queues, `atexit` handlers, cache entries keyed to a callback, worker pools handed a method to call. ### The second consequence: identity Because a new wrapper is built on every lookup, `obj.method is obj.method` is `False` while `obj.method == obj.method` is `True` — same `__func__`, same `__self__`, different wrapper objects. Two practical results: - Unregistering by identity fails silently. Code that scans a list comparing with `is`, or keys a dict on `id()`, will never find the entry it registered. Removal by equality (`list.remove`, or membership in a `set`, since bound methods hash consistently) does work. - If identity matters — you want to remove exactly what you added — bind once, keep that object, and register and unregister *that*. ### The fixes - **Own the lifecycle.** The simplest correct answer is usually an explicit `unregister` at the point the object is discarded, or a context manager around registration. Most "leaks" of this shape are missing removals, not missing weakness. - **Hold the instance weakly and re-bind at dispatch.** Store a weak reference to the instance plus the plain function, and reconstruct the call when firing; drop entries whose referent has died. The standard library also ships a weak-reference wrapper made specifically for bound methods, which packages exactly this. - **Register a plain function that takes the object as an argument**, and let the caller own the lifetime. - **Do not** try to fix it by storing `Cls.method` and passing the instance separately unless you have somewhere to keep the instance weakly — otherwise you have just moved the strong reference. ### The neighbouring facts interviewers check - `Cls.method` is the plain function, with no `__self__` at all. `Cls.method(obj)` is the unbound call and does the same work as `obj.method()`. - On a `@classmethod`, `__self__` is the *class*: `Cls.build.__self__ is Cls`. Since classes are usually module-level and immortal for the process, this rarely leaks anything interesting. - A `@staticmethod` looked up through either the class or an instance is just the function — no binding, no `__self__`, and `inspect.ismethod` returns `False` for it. - Built-in methods behave the same way: a list's `append` accessed off a list instance carries that list in `__self__`, which is why passing it as a callback pins the list. - `types.MethodType(func, obj)` constructs a bound method by hand, which is how you attach a callable to one single instance rather than to its class. - `functools.partial(Cls.method, obj)` produces a different object with the same retention property — the instance is still held strongly, just in the partial's arguments instead of in `__self__`. ### How to confirm it in a live process Take a weak reference to a representative object, drop your own reference, force a collection, and see whether the referent survives. If it does, walk the referrers to find who is holding it; a list of bound methods is unmistakable once you look at what the referring objects are. This is a retention problem rather than a cycle problem, so the cycle collector will never clear it: the reference is real, reachable and exactly as intended by the code that created it.

  • How would you register a callback without keeping the instance alive?
    Store a weak reference to the *instance* alongside the plain function and rebuild the call at dispatch time, dropping entries whose referent has died; the standard library also ships a weak-reference wrapper made specifically for bound methods. The often better answer is simpler: unregister explicitly when the object is discarded, since most leaks of this shape are a missing removal rather than a missing weak reference.
  • Why does obj.method is obj.method evaluate to False?
    Each attribute lookup constructs a new bound-method wrapper around the same function and the same instance, so identity differs while equality holds. Code that unregisters by identity, or keys a structure on `id()`, will silently miss the entry it added. If identity matters, bind once, keep that object, and register and remove exactly that one.
  • What is __self__ on a bound classmethod, and what does a staticmethod look like here?
    For a classmethod `__self__` is the class itself, so `Cls.build.__self__ is Cls` — and since classes normally live for the process, that pins nothing interesting. A staticmethod looked up through either the class or an instance is simply the plain function: no binding, no `__self__`, and `inspect.ismethod` returns `False` for it.

A bound method is a sticky note that says 'call this function with that object' — filing the note in a permanent drawer keeps the object in the building, however finished with it you are.

saying these in an interview costs you the question

  • Thinks obj.method returns the same object on every lookup
  • Says __self__ holds the instance weakly
  • Claims the cycle collector will clean it up anyway
  • Confuses Cls.method with a bound method object
  • Believes only reference cycles can pin objects in memory
  • Assumes functools.partial avoids retaining the instance

context