Back to Blog
Python

Python Method Binding: How Methods Attach to Objects

python method binding: Learn how Python binds methods to objects, the descriptor protocol, and the difference between bound, instance, class, and static methods.

descriptorsinstance methodsclass methodsstatic methodsPython internals
Diagram showing a Python function object binding to an instance to create a bound method.

When you access a method through an instance in Python, the function you retrieve is not the same object as the one defined in the class body. This behavior, known as python method binding, is what makes the instance automatically appear as the first argument to the method. Understanding this mechanism is essential for debugging, writing descriptors, and designing APIs that rely on method interception.

The Descriptor Protocol Behind Method Binding

Python functions are descriptors. A descriptor is an object that implements __get__, __set__, or __delete__. When a class attribute is accessed through an instance, Python calls __get__ on the attribute's value if that value defines it. For functions, __get__ returns a bound method object that wraps the original function and the instance.

class Greeter: def hello(self): return f"Hello from {self}" g = Greeter() print(g.hello) # <bound method Greeter.hello of <__main__.Greeter object at 0x...>>

The bound method object stores both the instance and the function. When you call it, Python passes the instance as the first argument automatically. This is why self appears without you explicitly providing it.

How Instance Methods Become Bound Methods

The binding happens at attribute access time, not at class definition time. When you write g.hello, Python looks up hello on the class, finds the function object, and calls its __get__ with the instance and the class as arguments. The function's __get__ returns a new bound method object each time.

print(g.hello is g.hello) # False

Each access creates a fresh bound method. This has performance and identity implications, which we'll examine later. The bound method object has a __self__ attribute pointing to the instance and a __func__ attribute pointing to the original function.

bound = g.hello print(bound.__self__) # <__main__.Greeter object at 0x...> print(bound.__func__) # <function Greeter.hello at 0x...>

Why the Instance Is Passed as the First Argument

The descriptor protocol is what makes self work. When you call g.hello(), Python evaluates g.hello first, producing a bound method, then calls it with no arguments. The bound method internally calls __func__(__self__). So the instance is injected as the first positional argument.

If you access the method on the class instead, you get the plain function, not a bound method.

print(Greeter.hello) # <function Greeter.hello at 0x...>

Calling Greeter.hello(g) works, but you must pass the instance explicitly. This is the unbound method behavior in Python 3, where the function is just a regular function with no binding.

classmethod and staticmethod: Changing the Binding Rule

The classmethod and staticmethod decorators change how the descriptor behaves. A classmethod binds the class, not the instance, as the first argument. It does this by wrapping the function in a descriptor that returns a bound method with the class as __self__.

class Counter: count = 0 @classmethod def increment(cls): cls.count += 1 return cls.count c = Counter() print(c.increment()) # 1 print(Counter.increment()) # 2

A staticmethod returns the underlying function unchanged. It does not bind anything, so you call it without an implicit first argument.

class MathUtil: @staticmethod def add(a, b): return a + b print(MathUtil.add(2, 3)) # 5

The choice between these decorators affects the API design. Use classmethod when the method needs to access class-level state or call other classmethods. Use staticmethod when the method is conceptually tied to the class but does not need access to class or instance data.

Bound Methods and Function Attributes

Bound methods are callable objects, but they also expose the underlying function's attributes. For example, you can inspect the function's __name__ or __doc__ through the bound method.

print(g.hello.__name__) # 'hello' print(g.hello.__doc__) # None

This can be useful when you need to introspect methods dynamically, such as in a framework that registers handlers. The bound method also supports __call__, so it behaves like a regular callable.

Performance and Memory Implications of Bound Methods

Creating a bound method object for every attribute access has runtime cost. In tight loops where you repeatedly call the same method through an instance, the overhead of creating a new bound method each time can be measurable. The cost is small, but it exists.

# Repeatedly accessing a method in a loop for _ in range(1000000): g.hello()

Each iteration creates a new bound method object. If you need to optimize, you can store the bound method in a local variable once and reuse it.

hello = g.hello for _ in range(1000000): hello()

This avoids repeated descriptor lookups and object creation. However, storing a bound method keeps a reference to the instance alive. If the instance is large and the bound method is stored in a long-lived container, the instance cannot be garbage collected until the bound method is released. This is a memory consideration when designing caches or registries.

Common Pitfalls with Method Binding

One frequent mistake is using a bound method as a default argument in a function definition. The binding happens at the time the default is evaluated, so the instance is captured and kept alive.

def callback(method=g.hello): method()

This can lead to unexpected retention of the instance. Similarly, storing bound methods in class attributes can cause subtle bugs if the class is redefined.

Another pitfall is assuming that g.hello is g.hello is true. Since each access creates a new bound method, identity checks fail. This matters when you write code that caches methods or relies on == comparisons. The bound method does not implement equality based on the instance and function; it uses default object identity.

When to Rely on Explicit Binding

Sometimes you need to control binding manually. The descriptor protocol allows you to create custom descriptors that return a different callable. For example, you might want a method that binds lazily or caches the bound method.

class LazyMethod: def __init__(self, func): self.func = func def __get__(self, instance, owner): if instance is None: return self.func return self.func(instance)

This is a simplified custom descriptor. In practice, you rarely need to implement your own binding unless you are building a framework, an ORM, or a decorator that intercepts method calls. Understanding the built-in binding behavior is enough for most applications.

When you need to pass a method as a callback without retaining the instance, you can use functools.partial to bind arguments explicitly, or you can use a lambda that calls the method. Both approaches give you control over what is captured.

import functools def callback(): return g.hello() # or callback = functools.partial(g.hello)

The partial object also holds a reference to the instance, so the same memory concern applies. If you want to avoid retention, you can store the instance in a weak reference and reconstruct the bound method when needed.

Python's method binding is a core mechanism that underpins object-oriented programming in the language. By understanding the descriptor protocol and the differences between bound, class, and static methods, you can write more predictable code and debug issues that arise from subtle binding behavior. The next time you see a TypeError about missing arguments or an unexpected self, you know exactly where the binding happened and why.

python method binding: Practical Usage and Code Examples | RYUSLOG DEV