Most Python tutorials explain @classmethod and @staticmethod with a toy example, list the surface-level differences, and move on. That leaves you with a memorized rule and no real understanding. So when you're building something real — a configuration parser, a model layer, a serialization framework — you still don't know which one to reach for. Or whether you need either at all. This article goes deeper. We'll trace where these decorators came from, what they do at the interpreter level, how they interact with inheritance, and when each one solves a problem that the other cannot. Real code, real comprehension.
A Regular Method, First
Before we can understand what @classmethod and @staticmethod modify, we need to be precise about what a regular instance method does. When you define a function inside a class body, Python's descriptor protocol transforms it during attribute access. The function's __get__ method fires and binds the instance as the first argument:
class Greeter:
def greet(self, name):
return f"Hello, {name}! I am {self}"
g = Greeter()
print(g.greet("Alice"))
# Hello, Alice! I am <__main__.Greeter object at 0x...>
The self parameter is not magic syntax. It is the result of the descriptor protocol intercepting g.greet and producing a bound method that pre-fills the first argument with g. Raymond Hettinger, a Python core developer, explains this mechanism in the official Python Descriptor HowTo Guide (docs.python.org): descriptors are used throughout the language, and it is how functions turn into bound methods.
@classmethod and @staticmethod are alternate descriptors. They change what happens during that __get__ interception. That is the entire mechanical difference, and everything else flows from it.
What @classmethod Actually Does
A class method receives the class itself as its first argument instead of an instance. By convention, this argument is named cls:
class Temperature:
def __init__(self, kelvin):
self.kelvin = kelvin
@classmethod
def from_celsius(cls, celsius):
return cls(celsius + 273.15)
@classmethod
def from_fahrenheit(cls, fahrenheit):
return cls((fahrenheit - 32) * 5 / 9 + 273.15)
def __repr__(self):
return f"Temperature({self.kelvin:.2f}K)"
>>> Temperature.from_celsius(100)
Temperature(373.15K)
>>> Temperature.from_fahrenheit(212)
Temperature(373.15K)
The critical detail is cls. It is not hardcoded to Temperature. It is whatever class the method was called on. This makes class methods polymorphic across inheritance hierarchies in a way that static methods and plain functions cannot be.
This pattern — alternate constructors — is the canonical use case for @classmethod. The from_celsius and from_fahrenheit methods each provide a different way to create a Temperature instance, complementing the default __init__ that takes Kelvin. Python's own standard library uses this pattern extensively: dict.fromkeys(), datetime.datetime.fromtimestamp(), int.from_bytes(), and pathlib.Path.cwd() are all class methods serving as alternate constructors.
The concept was part of a major language overhaul. Both classmethod and staticmethod were introduced in Python 2.2 (released December 2001) as part of PEP 252, which unified types and classes under the new-style class model. Guido van Rossum wrote in PEP 252 that class methods receive an implicit first argument that is the class for which they are invoked, noting that the concept has no direct C++ or Java equivalent.
The original syntax was verbose. Before decorators existed, you had to write the transformation after the function body:
class MyClass:
def foo(cls, y):
print(cls, y)
foo = classmethod(foo)
PEP 318 (accepted in 2004 for Python 2.4) introduced the @decorator syntax specifically because the old pattern was hard to read. The PEP states the problem directly: the current method for transforming functions and methods — for instance, declaring them as a class or static method — is awkward and can lead to code that is difficult to understand. For large functions, it separates a key component of the function's behavior from the definition of the rest of its external interface.
What @staticmethod Actually Does
A static method receives neither the instance nor the class as an implicit first argument. It is, mechanically, a plain function that happens to live inside a class's namespace:
class Validator:
def __init__(self, value):
if not self.is_positive(value):
raise ValueError(f"Expected positive number, got {value}")
self.value = value
@staticmethod
def is_positive(n):
return isinstance(n, (int, float)) and n > 0
>>> Validator.is_positive(5)
True
>>> Validator.is_positive(-3)
False
>>> v = Validator(10)
>>> v.is_positive(42)
True
The is_positive method doesn't need access to the instance (self) or the class (cls). It is a pure utility function: the same input always produces the same output, regardless of any object state. Placing it inside Validator rather than at module level is a namespacing decision — it communicates that this function is conceptually related to validation.
Under the hood, @staticmethod is a non-data descriptor whose __get__ method simply returns the original function unchanged. The Descriptor HowTo Guide in the Python documentation provides a pure Python equivalent that makes this transparent:
import functools
class StaticMethod:
"""Emulate PyStaticMethod_Type() in Objects/funcobject.c"""
def __init__(self, f):
self.f = f
functools.update_wrapper(self, f)
def __get__(self, obj, objtype=None):
return self.f
That is the entire mechanism. The __get__ method ignores both obj (the instance) and objtype (the class) and hands back the raw function. Compare this to a class method descriptor, which would wrap the function to prepend the class argument.
The Inheritance Test: Where the Difference Becomes Concrete
The distinction between @classmethod and @staticmethod is abstract until you introduce inheritance. This is where cls earns its keep.
Consider a base class that serializes data from JSON:
import json
class Model:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
@classmethod
def from_json(cls, json_string):
data = json.loads(json_string)
return cls(**data)
def __repr__(self):
attrs = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{type(self).__name__}({attrs})"
Now create subclasses:
class User(Model):
pass
class Product(Model):
pass
>>> User.from_json('{"name": "Alice", "email": "alice@example.com"}')
User(name='Alice', email='alice@example.com')
>>> Product.from_json('{"title": "Widget", "price": 9.99}')
Product(title='Widget', price=9.99)
from_json is defined once on Model, but when called on User, cls is User. When called on Product, cls is Product. The factory method automatically produces the correct type without any subclass needing to override it. This is polymorphic construction, and it only works because @classmethod passes the actual calling class through cls.
If from_json were a @staticmethod, you would have to hardcode the class name:
class Model:
@staticmethod
def from_json(json_string):
data = json.loads(json_string)
return Model(**data) # Always returns Model, never a subclass
Now User.from_json(...) returns a Model instance, not a User. Every subclass would need its own override. The @classmethod version avoids this entirely.
This is exactly why van Rossum came to see @classmethod as the more important of the two. In a July 2016 message on the python-ideas mailing list, he wrote:
"Honestly, staticmethod was something of a mistake — I was trying to do something like Java class methods but once it was released I found what was really needed was classmethod. But it was too late to get rid of staticmethod." — Guido van Rossum, python-ideas mailing list, July 2016
That quote is worth sitting with. The creator of Python considers @staticmethod a historical accident — an attempt to replicate a Java pattern that turned out to be the wrong abstraction for Python's class model.
When @staticmethod Is Still the Right Choice
Despite van Rossum's reservations, @staticmethod is not useless. It has legitimate applications, and understanding them prevents you from reaching for the wrong tool.
The clearest use case is a utility function that is logically bound to a class but does not need access to any class or instance state:
class MathVector:
def __init__(self, x, y):
self.x = x
self.y = y
def magnitude(self):
return self._hypot(self.x, self.y)
@staticmethod
def _hypot(a, b):
return (a ** 2 + b ** 2) ** 0.5
def __repr__(self):
return f"MathVector({self.x}, {self.y})"
_hypot is a pure computation. It does not read or modify anything on the class or instance. Making it a @staticmethod communicates this constraint explicitly. As the Real Python tutorial on instance, class, and static methods (realpython.com) puts it: flagging a method as a static method is a hint that the method won't modify class or instance state, and this restriction is also enforced by the Python runtime.
That enforcement is the value. A @staticmethod is a declaration of independence from object state. When another developer reads your class six months later, they can see at a glance that _hypot is a pure function. They do not need to scan its body to confirm it never touches self or cls. The decorator communicates intent before the reader processes a single line of implementation.
Another legitimate use case is when you need a callable in a class attribute slot that should not be bound as a method:
class EventProcessor:
default_handler = None
@staticmethod
def default_handler(event):
print(f"Unhandled event: {event}")
Without @staticmethod, Python's descriptor protocol would try to bind default_handler as a method, injecting self as the first argument when called on an instance. The @staticmethod decorator prevents this, ensuring the function receives only the arguments you explicitly pass.
When @classmethod Is the Right Choice
Class methods solve a specific set of problems that revolve around the class itself:
Alternate Constructors
The most common use case. When your class can be instantiated from multiple data formats or sources:
class Config:
def __init__(self, settings: dict):
self.settings = settings
@classmethod
def from_yaml(cls, filepath):
import yaml
with open(filepath) as f:
return cls(yaml.safe_load(f))
@classmethod
def from_env(cls):
import os
return cls(dict(os.environ))
@classmethod
def from_defaults(cls):
return cls({"debug": False, "log_level": "INFO"})
Class-Level State Management
When a method needs to read or modify attributes shared across all instances:
class ConnectionPool:
_pool = []
_max_size = 10
@classmethod
def configure(cls, max_size):
cls._max_size = max_size
@classmethod
def get_connection(cls):
if cls._pool:
return cls._pool.pop()
if len(cls._pool) < cls._max_size:
return cls._create_connection()
raise RuntimeError("Pool exhausted")
@classmethod
def _create_connection(cls):
# Subclasses can override to create different connection types
return {"type": cls.__name__, "status": "open"}
Pattern Enforcement in Subclasses
When a base class method needs to reference the actual subclass that invoked it:
class Serializable:
_registry = {}
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
cls._registry[cls.__name__] = cls
@classmethod
def deserialize(cls, data):
class_name = data.get("_type", cls.__name__)
target_cls = cls._registry.get(class_name, cls)
return target_cls(**{k: v for k, v in data.items() if k != "_type"})
The Google Style Guide Position
The Google Python Style Guide takes a notably strong stance. It states: "Never use @staticmethod unless forced to in order to integrate with an API defined in an existing library. Write a module level function instead. Use @classmethod only when writing a named constructor or a class-specific routine that modifies necessary global state such as a process-wide cache."
This is a defensible position. A module-level function is simpler, easier to test in isolation, and does not create the false impression of being bound to a class. If a function does not need self or cls, there is a real question about whether it belongs inside the class body at all.
However, the Google guide optimizes for large codebases with many contributors. In that context, the risk of @staticmethod being misused — hiding what should be a public utility, or preventing proper subclass overriding — outweighs the namespacing benefit. In a smaller codebase or a library with a well-defined API, the namespacing clarity of @staticmethod can be worth it.
The Descriptor Protocol: What Happens at Lookup Time
To truly understand the difference, you need to see what happens when Python resolves obj.method. The interpreter calls type(obj).__mro__ to walk the method resolution order, finds the attribute in a class __dict__, and checks whether it has a __get__ method. If it does, the descriptor protocol activates.
For a regular function, __get__ produces a bound method with the instance pre-filled. For a @classmethod, __get__ produces a bound method with the class pre-filled. For a @staticmethod, __get__ returns the raw function. You can observe this directly:
class Demo:
def instance_method(self):
pass
@classmethod
def class_method(cls):
pass
@staticmethod
def static_method():
pass
d = Demo()
# What __get__ returns for each:
print(type(Demo.__dict__["instance_method"].__get__(d, Demo)))
# <class 'method'>
print(type(Demo.__dict__["class_method"].__get__(d, Demo)))
# <class 'method'>
print(type(Demo.__dict__["static_method"].__get__(d, Demo)))
# <class 'function'>
The instance method produces a method bound to d. The class method produces a method bound to Demo. The static method produces a bare function — no binding at all. Three different descriptors, three different __get__ behaviors, all using the same underlying protocol.
A Decision Framework
When you are deciding which decorator to use (or whether to use one at all), ask these questions in order:
- Does the method need access to the instance (
self)? If yes, it is a regular instance method. No decorator needed. - Does the method need access to the class (
cls)? This includes alternate constructors, class-level configuration, or any situation where subclass polymorphism matters. If yes, use@classmethod. - Does the method need access to neither, but is logically coupled to the class? If it is a private helper that is only meaningful in the context of this class,
@staticmethodcommunicates that clearly. If it is a general-purpose utility, consider a module-level function instead. - Could this be a module-level function with no loss of clarity? If yes, make it a module-level function. Simpler is better.
Here is the framework applied to a real class:
class FileProcessor:
supported_extensions = {".txt", ".csv", ".json"}
def __init__(self, filepath):
if not self.is_supported(filepath):
raise ValueError(f"Unsupported file: {filepath}")
self.filepath = filepath
def process(self):
"""Needs self -- instance method."""
with open(self.filepath) as f:
return self._parse(f.read())
@classmethod
def from_directory(cls, dirpath):
"""Needs cls -- returns instances of the calling class."""
import pathlib
results = []
for path in pathlib.Path(dirpath).iterdir():
if cls.is_supported(str(path)):
results.append(cls(str(path)))
return results
@staticmethod
def is_supported(filepath):
"""Needs neither -- pure validation logic."""
import pathlib
return pathlib.Path(filepath).suffix in FileProcessor.supported_extensions
def _parse(self, content):
"""Needs self for subclass-specific parsing."""
return content
There is one subtle problem in this example, and it illustrates exactly why the @classmethod vs @staticmethod distinction matters. The is_supported method references FileProcessor.supported_extensions by name. If a subclass like CSVProcessor adds .tsv to its own supported_extensions, the static method will still check the parent class's set. A @classmethod version using cls.supported_extensions would correctly pick up the subclass override. Whether this matters depends on your design — and that judgment is the whole point of understanding the difference.
@classmethod and @staticmethod are not interchangeable decorators that you pick based on vibes. They are distinct descriptor implementations that determine what information a method receives at call time. @classmethod passes the class, enabling polymorphic construction and class-level operations that work correctly across inheritance hierarchies. @staticmethod passes nothing, declaring that a function is state-independent while namespacing it under a class.
Van Rossum introduced both in Python 2.2, later came to regard @staticmethod as a misstep, and the Google Python Style Guide goes so far as to discourage its use entirely. But the decorator persists because it fills a narrow, legitimate role: communicating that a function inside a class is deliberately decoupled from both instance and class state.
The question is never "which decorator is better." The question is "what does this method actually need access to?" Answer that, and the choice makes itself.
cd ..