Bound Methods vs Unbound Functions in Python 3

In Python 3, the distinction between bound methods and unbound functions comes down to how a function is accessed and whether an instance is automatically supplied as its first argument. When a function defined inside a class is accessed through an instance, it becomes a bound method that automatically binds the instance to self. When accessed directly through the class, it remains a standard, plain function—eliminating the Python 2 concept of "unbound methods" entirely. This article breaks down the mechanics, behavioral differences, and underlying descriptor protocol that govern these two callables.

What Is a Bound Method?

A bound method is a method that is dependent on an instance of a class. When you call a method through an object instance, Python automatically packages the instance and the function together.

class Greeter:
    def greet(self):
        return "Hello!"

obj = Greeter()
bound_greet = obj.greet

print(type(bound_greet))  # <class 'method'>

Because the method is bound to obj, calling bound_greet() automatically passes obj as the first argument (self).

Bound method objects have special read-only attributes:

What Is an Unbound Function?

In Python 2, accessing a method via the class (e.g., Greeter.greet) returned an "unbound method" type that enforced type-checking on the first argument. Python 3 eliminated this type.

In Python 3, retrieving a method directly from a class returns a standard function:

unbound_greet = Greeter.greet

print(type(unbound_greet))  # <class 'function'>

Because it is simply a function, it is not tied to any instance. To invoke it, you must explicitly pass an instance as the argument:

# Calling the function directly requires passing the instance
print(Greeter.greet(obj))  # Output: Hello!

If you call Greeter.greet() without an argument, Python raises a TypeError indicating that the required positional argument self is missing.

The Underlying Mechanism: The Descriptor Protocol

Functions in Python act as descriptors by implementing the __get__ method. The transformation from a function to a bound method occurs dynamically at runtime during attribute access:

  1. Access via an instance (obj.greet): Python calls Greeter.__dict__['greet'].__get__(obj, Greeter). Because an instance (obj) is provided, __get__ returns a bound method object wrapping both the function and the instance.

  2. Access via the class (Greeter.greet): Python calls Greeter.__dict__['greet'].__get__(None, Greeter). Because the instance argument is None, the __get__ implementation returns the function itself without wrapping it.

Key Differences Summary