Python Bound Method: How Instance Methods Work
python bound method: Understand how Python creates bound methods, why self is passed automatically, and how the descriptor protocol controls method access.
python bound method requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
When you access a method on an instance, Python does not return the raw function you defined in the class body. It returns a bound method object that already knows which instance to pass as self. This behavior is the result of the descriptor protocol, and it is central to how object-oriented Python works under the hood.
Consider this minimal class:
class Greeter: def greet(self, name): return f"Hello, {name}" obj = Greeter() print(obj.greet)
On Python 3, obj.greet prints something like <bound method Greeter.greet of <__main__.Greeter object at 0x...>>. The method is bound to obj, so calling obj.greet("Alice") is equivalent to Greeter.greet(obj, "Alice"). This automatic binding is what makes instance methods convenient, but it also introduces subtle pitfalls when you store or pass methods around.
What a Bound Method Object Contains
A bound method object wraps two pieces of information: the underlying function and the instance it is bound to. When you call the bound method, Python inserts the instance as the first argument, then forwards any additional arguments you provide.
class Counter: def __init__(self): self.count = 0 def increment(self, step=1): self.count += step c = Counter() bound_increment = c.increment bound_increment(2) print(c.count) # 2
The variable bound_increment holds a reference to the bound method. It retains the c instance even if you delete the original variable. This is a common source of memory leaks when methods are stored in long-lived collections.
How Attribute Lookup Creates a Bound Method
Python's attribute lookup for obj.method follows a specific order. It first checks the instance's __dict__, then the class and its bases. When the attribute is found on the class and it is a descriptor that defines __get__, Python calls that descriptor's __get__ method with the instance and class.
Functions are descriptors. Their __get__ method returns a bound method when accessed through an instance, or the plain function when accessed through the class.
class Sample: def method(self): pass print(Sample.method) # <function Sample.method at 0x...> print(Sample().method) # <bound method Sample.method of ...>
This distinction matters when you need to pass a method as a callback. Passing obj.method gives you a callable that already has self fixed. Passing Sample.method gives you a plain function that expects the instance as its first argument.
Bound vs Unbound Methods in Python 3
In Python 2, accessing a method on a class returned an unbound method that required an instance as the first argument. Python 3 removed this distinction. Now Class.method is just a regular function. There is no separate unbound method type.
This change simplifies the language, but it also means you cannot rely on type checks that distinguish bound and unbound methods. The types.MethodType type still exists for bound methods, but plain functions are just types.FunctionType.
import types class A: def f(self): pass print(type(A.f)) # <class 'function'> print(type(A().f)) # <class 'method'>
When you need to call a class function manually with an explicit instance, you can do so: A.f(obj). This works because the function does not enforce that its first argument is named self; it simply passes whatever you supply.
Common Mistakes with Bound Methods
A frequent error is storing a method reference and later calling it without realizing that self is already bound. For example, in GUI toolkits or event handlers, you might write:
class Button: def on_click(self): print("clicked") button = Button() handler = button.on_click # bound method, self is captured # Later, when the event fires: handler() # works fine
But if you accidentally store the class function instead:
handler = Button.on_click # plain function # Later: handler() # TypeError: on_click() missing 1 required positional argument: 'self'
Another common issue is passing a bound method to a function that expects a callable with a specific signature. Since the bound method already includes self, the effective signature is just the remaining parameters. If you pass a plain function, you must account for self manually.
Performance and Runtime Cost of Bound Methods
Creating a bound method object is not free. Every access to obj.method allocates a new method object. In tight loops or frequently called code, this allocation can add measurable overhead.
class MathOps: def add(self, a, b): return a + b ops = MathOps() for _ in range(1000): result = ops.add(1, 2) # creates a new bound method each iteration
If you need to call the same method many times, you can cache the bound method outside the loop:
add_method = ops.add for _ in range(1000): result = add_method(1, 2)
This avoids repeated allocation. However, caching a bound method keeps the instance alive. If the instance is large or holds resources, this can cause memory retention. Use caching judiciously, especially in long-running processes.
When to Use functools.partial or Lambda Instead
Sometimes you need to bind arguments other than self. For example, you might want a callable that always passes a fixed argument to a method. functools.partial is a cleaner alternative to a lambda because it preserves the function's metadata and is more readable.
from functools import partial class Greeter: def greet(self, name, punctuation="!"): return f"Hello, {name}{punctuation}" g = Greeter() greet_alice = partial(g.greet, "Alice") print(greet_alice()) # Hello, Alice! print(greet_alice("?")) # Hello, Alice?
A lambda can do the same but is often less explicit:
greet_alice = lambda: g.greet("Alice")
Use partial when you need to pre-fill positional or keyword arguments. Use a lambda when you need to transform the arguments or call multiple methods. Both avoid creating a new bound method on each call, but they also capture the instance, so the same memory consideration applies.
Advanced: Custom Descriptors and Method Binding
The descriptor protocol is not limited to functions. You can create your own descriptor that returns a custom callable when accessed through an instance. This is how libraries like staticmethod and classmethod are implemented.
class StaticMethod: def __init__(self, func): self.func = func def __get__(self, obj, objtype=None): return self.func class MyClass: @StaticMethod def helper(): return "static" print(MyClass.helper()) # static
Similarly, a custom descriptor can return a bound method-like object that carries additional state. This pattern is useful for building method decorators that need to inspect or modify the call at runtime.
class BoundLogger: def __init__(self, func, instance): self.func = func self.instance = instance def __call__(self, *args, **kwargs): print(f"Calling {self.func.__name__} on {self.instance}") return self.func(self.instance, *args, **kwargs) class Logged: def __init__(self): self.value = 0 def set_value(self, new): self.value = new def __getattribute__(self, name): attr = super().__getattribute__(name) if callable(attr) and name.startswith("set"): return BoundLogger(attr, self) return attr obj = Logged() obj.set_value(5) # prints: Calling set_value on <...>
This example overrides __getattribute__ to wrap specific methods, but it illustrates the principle: binding is just a convention that the descriptor protocol makes configurable. Understanding this lets you debug why a method reference behaves differently than expected and design APIs that control method access precisely.