2025, Dec 23 15:00

Avoid stale bound methods in Python: dynamic dispatch that always targets the current instance

Learn why caching bound methods in Python dicts calls stale instances after resets, and fix it with lambda wrappers or rebinding handlers for dynamic dispatch.

Dynamic dispatch in Python is powerful until it quietly points at the wrong object. A common pitfall is storing a method in a structure like a dict and later replacing the instance the method was originally bound to. The callable in the dict still targets the original object, so after a reset you end up invoking the outdated instance. Here’s a concise demonstration and a reliable fix.

Reproducing the behavior

class AttrNode:
    def __init__(self):
        self.value = 0

    def show_dummy(self):
        print('id : ' + hex(id(self)))
        print(self.value)

    def set_dummy(self, new_val):
        self.value = new_val


class Box:
    def __init__(self):
        self.member = AttrNode()
        self.handlers = {'dyn': self.member.show_dummy}

    def reset_member(self):
        self.member = AttrNode()


def run():
    box = Box()
    print('--A--: create instance of Box')
    box.member.show_dummy()             # direct call
    box.handlers['dyn']()               # dynamic call via dict

    print('--B--: call to set_dummy(5)')
    box.member.set_dummy(5)
    box.member.show_dummy()             # direct call reflects update
    box.handlers['dyn']()               # dynamic call reflects update

    print('--C--: call the reset function')
    box.reset_member()                  # replace the instance
    box.member.show_dummy()             # direct call hits the new instance
    box.handlers['dyn']()               # dynamic call still hits the old one


if __name__ == '__main__':
    run()

Why this happens

The core issue is a cached bound method. When you assign self.member.show_dummy into a dict, you aren’t storing a reference to the Box or to self.member. You store a bound method object, which holds its own reference to the specific instance that existed at assignment time. Replacing self.member with a new AttrNode doesn’t change the already stored bound method; it still points to the old instance.

The fix

Instead of caching the bound method, defer method resolution to call time. One practical way is to wrap the call in a lambda. The wrapper looks up the current self.member each time, ensuring the call always targets the latest instance.

class AttrNode:
    def __init__(self):
        self.value = 0

    def show_dummy(self):
        print('id : ' + hex(id(self)))
        print(self.value)

    def set_dummy(self, new_val):
        self.value = new_val


class Box:
    def __init__(self):
        self.member = AttrNode()
        self.handlers = {'dyn': lambda: self.member.show_dummy()}

    def reset_member(self):
        self.member = AttrNode()


def run():
    box = Box()
    box.handlers['dyn']()       # calls current member.show_dummy()
    box.member.set_dummy(5)
    box.handlers['dyn']()       # still current
    box.reset_member()
    box.handlers['dyn']()       # now the new instance


if __name__ == '__main__':
    run()

The lambda forwards whatever the wrapped method returns. If the target method returns data (for example, a list), the wrapper returns the same data. Make sure you call the method inside the lambda with parentheses; omitting them would store the function object again instead of invoking it.

There is another straightforward approach: rebind the stored callable whenever you replace the instance. After you create a new AttrNode in reset_member, assign the fresh bound method back into the dict.

class Box:
    def __init__(self):
        self.member = AttrNode()
        self.handlers = {'dyn': self.member.show_dummy}

    def reset_member(self):
        self.member = AttrNode()
        self.handlers['dyn'] = self.member.show_dummy

Why this detail matters

Method objects in Python close over the instance they’re bound to. If you stash them away for later, you’ve effectively frozen that relationship. In systems that swap components at runtime, this can silently route calls to stale objects, leading to unexpected state, confusing logs, or data mismatches after a reset. Understanding how bound methods capture their owner helps you avoid subtle bugs in dynamic dispatch, plugin registries, callback tables, and event maps.

Practical takeaways

When you need dynamic calls to always hit the current attribute, avoid caching bound methods. Use a small wrapper like lambda to resolve the attribute at call time. If you prefer caching, remember to rebind the handler after every reset so it points to the new instance. In both cases you preserve behavior while ensuring the target stays fresh.

Keep this pattern in mind any time you store callables from instance attributes. It’s a small change that prevents elusive bugs when objects are rotated or reinitialized.