Bound, Unbound, and Static Methods in Python: Complete Method Types and Usage Implementation Guide

Bound, unbound, and static methods in python

I used to think “method” was just one thing — a function inside a class. Then I hit an error message referencing “unbound method” in some old Python 2 code I was porting, followed by a completely different error involving “missing 1 required positional argument: ‘self'” in my own Python 3 code, and I realized there was a whole taxonomy here I hadn’t actually learned. This guide walks through bound methods, the (mostly historical) concept of unbound methods, static methods, and class methods — what each one is, how it’s created, and when to use it.

Functions vs Methods: The Starting Point

A function defined inside a class body is, at the moment it’s defined, just a plain function object:

class Dog:
    def bark(self):
        return "Woof"

print(type(Dog.__dict__["bark"]))  # <class 'function'>

It only becomes something more interesting when it’s accessed — because functions implement the descriptor protocol (__get__), accessing bark through the class or an instance triggers different behavior depending on how you access it.

Bound Methods

When you access a method through an instance, Python creates a bound method — an object that pairs the function with that specific instance, so self is supplied automatically.

class Dog:
    def bark(self):
        return "Woof"

d = Dog()
print(d.bark)          # <bound method Dog.bark of <__main__.Dog object at 0x...>>
print(type(d.bark))    # <class 'method'>
print(d.bark())        # Woof — no need to pass d explicitly

Under the hood, d.bark triggers Dog.__dict__["bark"].__get__(d, Dog), and a plain function’s __get__ returns a method object wrapping both the function and d. Calling d.bark() is equivalent to calling Dog.bark(d):

print(Dog.bark(d))     # Woof — identical to d.bark()

This is why every instance method needs self as its first parameter — it’s not special syntax, it’s just the first positional argument, which the bound method fills in automatically with the instance.

You can extract and store a bound method independently, and it keeps working correctly because it already carries a reference to the instance:

say_woof = d.bark
print(say_woof())  # Woof — still works, still bound to d

Unbound Methods (Python 2 Legacy)

In Python 2, accessing a method through the class (not an instance) produced an unbound method — a distinct wrapper type that checked its first argument was an instance of the correct class, but didn’t supply that argument for you:

# Python 2 concept (illustrative, not valid Python 3):
# Dog.bark  ->  <unbound method Dog.bark>
# Dog.bark(d)  ->  Woof
# Dog.bark("not a dog")  ->  TypeError: unbound method requires a Dog instance

Python 3 removed the unbound method type entirely. In Python 3, accessing a method via the class simply returns the plain function object, with no special wrapper and no instance-type checking:

class Dog:
    def bark(self):
        return "Woof"

print(Dog.bark)          # <function Dog.bark at 0x...>
print(type(Dog.bark))    # <class 'function'>
print(Dog.bark(d))       # Woof — works, self supplied manually
print(Dog.bark("hi"))    # TypeError: 'str' object has no attribute... (fails differently, not via type-check)

This simplification is one of the genuinely underrated cleanups in the Python 2-to-3 transition — one less concept to hold in your head, since Class.method now behaves just like accessing any other plain function.

Static Methods

A static method belongs to a class conceptually (it lives in the class’s namespace, often because it’s logically related to the class) but doesn’t receive self or cls at all — it behaves like a regular function that just happens to be reachable through the class.

class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtils.add(2, 3))       # 5
m = MathUtils()
print(m.add(2, 3))               # 5 — works via instance too, no self passed

staticmethod is, itself, a descriptor — its __get__ simply returns the raw underlying function, unwrapped, regardless of whether it’s accessed via the class or an instance. That’s exactly why no self or cls gets bound in.

Use static methods for utility logic that’s thematically related to a class but doesn’t need access to instance or class state:

class TemperatureConverter:
    @staticmethod
    def celsius_to_fahrenheit(c):
        return c * 9 / 5 + 32

    @staticmethod
    def fahrenheit_to_celsius(f):
        return (f - 32) * 5 / 9

print(TemperatureConverter.celsius_to_fahrenheit(100))  # 212.0

Class Methods

A class method receives the class itself as its first argument (conventionally named cls), not an instance — regardless of whether you call it on the class or an instance.

class Pizza:
    def __init__(self, toppings):
        self.toppings = toppings

    @classmethod
    def margherita(cls):
        return cls(["tomato", "mozzarella"])

    @classmethod
    def pepperoni(cls):
        return cls(["tomato", "mozzarella", "pepperoni"])

p = Pizza.margherita()
print(p.toppings)  # ['tomato', 'mozzarella']

This is the alternative constructor pattern, extremely common in real code — classmethod lets you offer multiple named ways to build an object without overloading __init__ with confusing conditional logic. Crucially, cls means class methods respect subclassing correctly:

class StuffedCrustPizza(Pizza):
    pass

sp = StuffedCrustPizza.margherita()
print(type(sp))  # <class 'StuffedCrustPizza'>, not Pizza!

Because margherita uses cls(...) rather than hardcoding Pizza(...), calling it on a subclass constructs an instance of the subclass, not the base class — this is exactly why classmethod is preferred over staticmethod for factory functions.

Comparing All Three Side by Side

class Demo:
    def instance_method(self):
        return f"instance_method called with self={self}"

    @classmethod
    def class_method(cls):
        return f"class_method called with cls={cls}"

    @staticmethod
    def static_method():
        return "static_method called with no implicit args"

d = Demo()
print(d.instance_method())  # self is the instance d
print(d.class_method())     # cls is Demo, even called via instance
print(d.static_method())    # no implicit argument at all

print(Demo.class_method())  # still cls=Demo, works fine via class
# Demo.instance_method()    # TypeError: missing 1 required positional argument: 'self'

Calling Demo.instance_method() without an instance fails, because there’s no bound instance to supply as self — you’d need Demo.instance_method(some_instance) explicitly, exactly as in the unbound-method discussion above.

How This Maps to the Descriptor Protocol

All three behaviors come from three different __get__ implementations:

Decorator__get__ behaviorFirst argument received
(none, plain function)returns bound method object when accessed via instanceself (the instance)
@classmethodalways binds to the class, even via instancecls (the class)
@staticmethodreturns the raw function, unbound to anythingnothing implicit

If you’re comfortable with descriptors, this table is really the entire concept — three different __get__ policies producing three different calling conventions.

Common Mistakes

  • Forgetting @staticmethod/@classmethod and calling a plain function via the class, which then requires manually passing whatever the function’s first parameter is meant to represent.
  • Using @staticmethod when @classmethod was actually needed, especially for factory methods — this breaks correct behavior under subclassing, as shown with StuffedCrustPizza above.
  • Hardcoding the class name inside a method meant to be inherited, instead of using cls, which silently breaks subclass behavior.
  • Assuming Python 3 still has “unbound methods” — the term shows up in a lot of older tutorials and Stack Overflow answers, but it doesn’t apply to Python 3’s model.

FAQs

Q: Can I call a static method without ever instantiating the class? Yes — that’s the whole point. MathUtils.add(2, 3) works with zero MathUtils instances ever created.

Q: Is there a performance difference between static methods and plain module-level functions? Negligible for typical use; the difference is organizational (keeping related utility logic namespaced under the class) rather than about speed.

Q: Why does cls matter more than it looks like it should? Because cls(...) inside a classmethod calls whatever class it was actually invoked on — critical for factory methods to behave correctly across a whole inheritance hierarchy, as shown by the Pizza/StuffedCrustPizza example.

Summary

Python has three practical method flavors reachable through a class: bound instance methods (which auto-supply self), static methods (which supply nothing implicit and behave like plain functions namespaced under a class), and class methods (which auto-supply cls, the actual class used to invoke them — vital for correct subclassing behavior in factory methods). “Unbound methods” were a real, distinct Python 2 concept that Python 3 deliberately removed in favor of just returning plain functions. All of this behavior is powered by the descriptor protocol under the hood — different __get__ implementations producing different calling conventions.

References

Total
0
Shares

Leave a Reply

Previous Post
Introduction to classes in python

Introduction to Classes in Python: Complete Object-Oriented Programming Fundamentals and Implementation

Next Post
Basic inheritance in python

Basic Inheritance in Python: Complete Object-Oriented Programming Subclass and Superclass Guide

Related Posts