31.5 Descriptors in the Standard Library: classmethod, staticmethod
While property is the most common descriptor encountered by Python developers, the language’s standard library includes two other critical descriptors that are fundamental to class design: classmethod and staticmethod. These built-in types are implemented as descriptors to provide a clean, consistent API for defining methods that do not operate on a specific instance.
The classmethod Descriptor
A classmethod is a method that receives the class itself (cls) as its implicit first argument, rather than an instance (self). This makes it ideal for factory methods, alternative constructors, or any method that needs to operate at the class level, potentially accessing or modifying class state.
The descriptor protocol is what makes this automatic binding of the class possible. When you access a classmethod from a class, the descriptor’s __get__ method is invoked. Instead of returning the raw function, it returns a new bound method object that, when called, automatically prepends the owning class to the argument list.
class Planet:
NUM_MOONS = 0 # Class-level state
def __init__(self, name):
self.name = name
@classmethod
def get_moon_count(cls):
"""Operates on the class, not an instance."""
return cls.NUM_MOONS
@classmethod
def earth(cls):
"""An alternative constructor (factory method)."""
# Notice 'cls' is used, making it polymorphic for subclasses.
return cls(name="Earth")
# Accessing from the class: descriptor returns a bound method
bound_method = Planet.get_moon_count
print(bound_method) # Output: <bound method Planet.get_moon_count of <class '__main__.Planet'>>
print(bound_method()) # Output: 0
# Accessing from an instance: descriptor still binds the class!
mars = Planet("Mars")
print(mars.get_moon_count()) # Output: 0. The class is still bound.
# Using the factory method
earth_instance = Planet.earth()
print(earth_instance.name) # Output: Earth
A crucial and often overlooked feature of classmethod is its polymorphism. When a class method is inherited by a subclass, the cls argument passed to it is always the subclass that made the call. This allows class methods to work seamlessly with inheritance, making them perfect for creating flexible factory patterns.
class GasGiant(Planet):
NUM_MOONS = 80 # Overrides the class variable
jupiter = GasGiant("Jupiter")
# Calls the inherited method, but cls is GasGiant, not Planet
print(jupiter.get_moon_count()) # Output: 80
The staticmethod Descriptor
In contrast, a staticmethod is a method that does not receive an implicit first argument. It is simply a function that happens to live inside a class namespace. The descriptor protocol for staticmethod is simpler: its __get__ method returns the underlying function unchanged, regardless of whether it’s accessed from the class or an instance.
You use @staticmethod when the method has a logical connection to the class but does not need to access either instance state (self) or class state (cls). Common use cases include utility functions, conversion routines, or grouping related functionality.
class TemperatureConverter:
@staticmethod
def celsius_to_fahrenheit(c):
"""A pure function; no dependency on class or instance state."""
return (c * 9/5) + 32
@staticmethod
def fahrenheit_to_celsius(f):
return (f - 32) * 5/9
# No binding occurs. The descriptor returns the function itself.
func = TemperatureConverter.celsius_to_fahrenheit
print(func) # Output: <function TemperatureConverter.celsius_to_fahrenheit at 0x...>
# Called on the class: works as a namespaced function
print(TemperatureConverter.celsius_to_fahrenheit(100)) # Output: 212.0
# Called on an instance: also works, same result
converter = TemperatureConverter()
print(converter.fahrenheit_to_celsius(212)) # Output: 100.0
Key Differences and Common Pitfalls
The most common pitfall is confusing these decorators and misapplying them. Using @staticmethod when you need @classmethod is a frequent error.
classmethodvs.staticmethod: If a method needs to know which class it’s being called on (e.g., to read a class variable or call another class method), it must be aclassmethod. Astaticmethodhas no access toclsand therefore cannot see the class’s state or other class methods. If a method doesn’t needselforcls, then astaticmethodis appropriate, though often a module-level function can be a simpler alternative.Inheritance Behavior: This is a critical distinction. A
classmethodis polymorphic; it will receive the calling subclass ascls. Astaticmethodis oblivious to inheritance; it will always behave the same, regardless of which subclass uses it.Calling from an Instance: While both can be called from an instance,
classmethodis bound to the class, not the instance. This can lead to confusion if you expectselfto be available inside aclassmethod—it won’t be.
class Example:
class_data = "class data"
@classmethod
def class_method(cls):
return f"Class method accessing {cls.class_data}"
@staticmethod
def static_method():
# This line would cause a NameError: 'class_data' is not defined
# return f"Static method trying to access {class_data}"
return "Static method can't see class_data"
# This works perfectly
print(Example.class_method()) # Output: Class method accessing class data
# This demonstrates the limitation of staticmethod
print(Example.static_method()) # Output: Static method can't see class_data
In summary, classmethod and staticmethod are powerful tools implemented as descriptors to control method binding. Understanding their underlying mechanism—the descriptor protocol—explains why they behave the way they do and allows you to wield them correctly to write cleaner, more intentional, and more polymorphic class designs.