Difference Between @staticmethod and @classmethod in Python

Every time you call a method in Python, the interpreter hands something to it before your arguments arrive. For a regular instance method, Python hands the instance. For a @classmethod, Python hands the class. For a @staticmethod, Python hands nothing at all. That single difference — what gets handed to the method — determines everything: what state it can reach, how it behaves when inherited, and what role it plays in class design. This article traces that difference from its mechanical root in the descriptor protocol through every practical consequence, including factory patterns, multiple inheritance, type checking, and version-specific behaviour in Python 3.10 and 3.13.

You have already used class methods

If you have ever called datetime.fromtimestamp(), date.today(), Path.cwd(), or int.from_bytes() in the Python standard library, you were calling a @classmethod. These are all factory methods — alternative constructors that create instances from different input formats. The cls parameter is what allows them to return the correct subclass when subclassed, instead of always returning the base type.

Regular instance methods, class methods, and static methods all live in the class body and are all accessed through the same attribute lookup mechanism. The only thing that differs is what Python injects as the first argument when the method is called.

The Three Kinds of Methods

Every method defined inside a class falls into one of three categories based on its decorator. Here is a class that defines all three side by side:

class Demo:
    class_var = "shared"

    def __init__(self, value: str) -> None:
        self.instance_var = value

    # Instance method: receives the instance as 'self'
    def instance_method(self) -> str:
        return f"instance_method called on {self.instance_var}"

    # Class method: receives the class as 'cls'
    @classmethod
    def class_method(cls) -> str:
        return f"class_method called on {cls.__name__}, class_var={cls.class_var}"

    # Static method: receives nothing automatically
    @staticmethod
    def static_method(x: int, y: int) -> str:
        return f"static_method called with {x} and {y}"

obj = Demo("hello")

print(obj.instance_method())
# instance_method called on hello

print(Demo.class_method())
# class_method called on Demo, class_var=shared

print(Demo.static_method(3, 4))
# static_method called with 3 and 4

The instance method instance_method receives the instance (self) and can access both instance attributes (self.instance_var) and class attributes (self.class_var or self.__class__.class_var). The class method class_method receives the class (cls) and can access class attributes (cls.class_var) but not instance attributes, because it has no instance. The static method static_method receives neither and operates only on its explicit arguments.

Method type
What Python hands it
Can access
instance method
The instance (self)
Instance state + class state
@classmethod
The class (cls)
Class state only
@staticmethod
Nothing
Only explicit arguments
Note

The names self and cls are conventions, not keywords. You could name them anything, but breaking this convention makes code harder to read and violates expectations that every Python developer relies on.

How They Work: The Descriptor Protocol

Both @staticmethod and @classmethod are implemented as non-data descriptors -- objects that define __get__() but neither __set__() nor __delete__(). This classification matters: non-data descriptors can be shadowed by instance attributes, whereas data descriptors (like property) take precedence over an instance's __dict__. The Python documentation describes them as part of the same non-data descriptor protocol as ordinary functions: "Python methods (including staticmethod() and classmethod()) are implemented as non-data descriptors." (Python Descriptor HowTo Guide)

When you access a method on a class or instance, Python calls the descriptor's __get__ to determine what to return.

For a @staticmethod, the descriptor returns the bare function with no binding. The Python documentation provides this canonical pure-Python equivalent, emulating PyStaticMethod_Type() in Objects/funcobject.c:

import functools

# Canonical pure-Python equivalent of @staticmethod
# Source: docs.python.org/3/howto/descriptor.html
class StaticMethod:
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"
    def __init__(self, f):
        self.f = f
        functools.update_wrapper(self, f)  # copies __name__, __qualname__, __doc__

    def __get__(self, obj, objtype=None):
        return self.f  # return the bare function, no binding

    def __call__(self, *args, **kwds):
        return self.f(*args, **kwds)

The functools.update_wrapper() call in the pure-Python emulation attaches a __wrapped__ attribute pointing to the underlying function, which is useful for introspection tools and decorators that use inspect.signature(). In the real built-in staticmethod, __wrapped__ was added natively in Python 3.10 — no functools required. (Python Built-in Functions, Python 3.10 changelog) The key behavior is in __get__: it always returns self.f unchanged, regardless of whether it was accessed from an instance or a class.

For a @classmethod, the descriptor binds the class as the first argument using a closure. This is the canonical pure-Python equivalent from the Python documentation, emulating PyClassMethod_Type() in Objects/funcobject.c:

# Canonical pure-Python equivalent of @classmethod
# Source: docs.python.org/3/howto/descriptor.html
class ClassMethod:
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"
    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

When you call Demo.class_method(), the descriptor intercepts the attribute access, captures Demo as klass in the closure, and returns newfunc. When that callable is invoked, it calls the original function with klass prepended as the first argument. When you call Demo.static_method(3, 4), the descriptor returns the raw function with no binding at all.

Python 3.13 Version Change

The ability to wrap other descriptors (such as @property) with @classmethod was added in Python 3.9, deprecated in Python 3.11, and fully removed in Python 3.13. The Python 3.13 release notes state the feature was removed because the core design was flawed and led to several problems. (What's New in Python 3.13) Any code using @classmethod around @property will raise a TypeError at runtime on Python 3.13 and must be rewritten. The recommended workaround noted in the release documentation is to use the __wrapped__ attribute (available on both decorators since Python 3.10) to pass through a classmethod without chaining descriptors. (Python Built-in Functions)

@classmethod Use Cases

Factory Methods (Alternative Constructors)

The primary use case for @classmethod is building factory methods that create instances through alternative pathways. Python supports only one __init__ per class. When you need to create objects from different input formats, class methods provide named constructors:

from datetime import date

class Employee:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name: str, birth_year: int) -> "Employee":
        """Create an Employee from a birth year instead of an age."""
        age = date.today().year - birth_year
        return cls(name, age)

    @classmethod
    def from_dict(cls, data: dict) -> "Employee":
        """Create an Employee from a dictionary."""
        return cls(data["name"], data["age"])

    def __repr__(self) -> str:
        return f"Employee({self.name!r}, age={self.age})"

# Three ways to create the same kind of object
e1 = Employee("Alice", 30)
e2 = Employee.from_birth_year("Bob", 1990)
e3 = Employee.from_dict({"name": "Carol", "age": 28})

print(e1)  # Employee('Alice', age=30)
print(e2)  # Employee('Bob', age=36)
print(e3)  # Employee('Carol', age=28)

The critical detail is cls(name, age) instead of Employee(name, age). Using cls ensures that if a subclass inherits from_birth_year, it creates an instance of the subclass, not the parent class. This is class method polymorphism.

Spot the Bug

This factory class has a bug that will silently break subclass behaviour. Read the code carefully — the bug is real, subtle, and appears in production code all the time. Can you spot it?

from datetime import date

class Employee:
    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name: str, birth_year: int) -> "Employee":
        age = date.today().year - birth_year
        return Employee(name, age)   # <-- look here

    def __repr__(self) -> str:
        return f"Employee({self.name!r}, age={self.age})"

class Manager(Employee):
    def __init__(self, name: str, age: int, reports: int = 0) -> None:
        super().__init__(name, age)
        self.reports = reports

    def __repr__(self) -> str:
        return f"Manager({self.name!r}, age={self.age}, reports={self.reports})"

m = Manager.from_birth_year("Alice", 1990)
print(type(m))   # what do you expect?

What is the bug, and what does type(m) actually print?

Accessing and Modifying Class State

Class methods can read and modify class-level attributes that are shared across all instances:

class ConnectionPool:
    _max_connections: int = 10
    _active: int = 0

    def __init__(self, host: str) -> None:
        if type(self)._active >= type(self)._max_connections:
            raise RuntimeError("Connection pool exhausted")
        self.host = host
        type(self)._active += 1

    @classmethod
    def set_max_connections(cls, n: int) -> None:
        """Adjust the pool size for all future connections."""
        cls._max_connections = n

    @classmethod
    def status(cls) -> str:
        return f"{cls._active}/{cls._max_connections} connections active"

    def close(self) -> None:
        type(self)._active -= 1

ConnectionPool.set_max_connections(5)
conns = [ConnectionPool("db.example.com") for _ in range(3)]
print(ConnectionPool.status())  # 3/5 connections active

Note that __init__ and close() use type(self)._active rather than hardcoding ConnectionPool._active. This is the subclass-safe idiom: if a subclass inherits ConnectionPool, type(self) resolves to the subclass at runtime, keeping the counter scoped to the correct class. Hardcoding the parent class name would cause all subclasses to share the same counter regardless of inheritance.

Using super() Inside @classmethod

A question that trips up many developers is whether super() works inside a @classmethod. It does — and it behaves exactly as you would expect. When called from a class method, super() returns a proxy that delegates attribute lookup to the next class in the MRO (Method Resolution Order), with cls as the starting point.

class Animal:
    sound: str = "..."

    @classmethod
    def describe(cls) -> str:
        return f"I am a {cls.__name__} and I say {cls.sound}"

    @classmethod
    def create(cls) -> "Animal":
        return cls()

class Dog(Animal):
    sound = "woof"

    @classmethod
    def describe(cls) -> str:
        # super() inside a classmethod works correctly
        base = super().describe()
        return f"{base} (domestic)"

print(Dog.describe())
# I am a Dog and I say woof (domestic)

# super().create() calls Animal.create with cls=Dog still bound
# because super() only changes the MRO lookup, not the cls binding
dog = super(Dog, Dog).create()
print(type(dog))  # <class '__main__.Dog'>

The subtlety is that super() inside a @classmethod changes where Python looks up the next method in the chain, but it does not change what cls is. If Dog.describe() calls super().describe(), Python looks up describe on Animal, but cls is still Dog — so cls.__name__ returns 'Dog', not 'Animal'. This is what makes cooperative factory hierarchies work correctly without losing the subclass identity.

@staticmethod Use Cases

Utility Functions Namespaced to a Class

Static methods are for functions that belong to a class conceptually but do not need access to the class or any instance. They serve as namespaced utility functions:

class TemperatureConverter:
    """Namespace for temperature conversion functions."""

    @staticmethod
    def celsius_to_fahrenheit(c: float) -> float:
        return c * 9 / 5 + 32

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

    @staticmethod
    def celsius_to_kelvin(c: float) -> float:
        return c + 273.15

print(TemperatureConverter.celsius_to_fahrenheit(100))  # 212.0
print(TemperatureConverter.fahrenheit_to_celsius(72))   # 22.222...

None of these functions need self or cls. They could be module-level functions, but placing them inside TemperatureConverter groups them logically and makes them discoverable through the class's namespace.

Validation Helpers Called by Instance Methods

A common pattern is using static methods as validation helpers that instance methods or class methods call internally:

import re

class User:
    def __init__(self, name: str, email: str) -> None:
        if not self.validate_email(email):
            raise ValueError(f"Invalid email: {email}")
        self.name = name
        self.email = email

    @staticmethod
    def validate_email(email: str) -> bool:
        """Return True if email matches a basic address pattern."""
        pattern = r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"
        return bool(re.fullmatch(pattern, email))

    @classmethod
    def from_string(cls, user_string: str) -> "User":
        """Parse 'Name <email>' format and return a new User."""
        match = re.match(r"(.+)\s+<(.+)>", user_string)
        if not match:
            raise ValueError(f"Cannot parse: {user_string}")
        name, email = match.groups()
        return cls(name, email)

# Static method used for standalone validation
print(User.validate_email("[email protected]"))  # True
print(User.validate_email("not-an-email"))       # False

# Class method as factory, static method called internally by __init__
user = User.from_string("Alice <[email protected]>")
print(user.name, user.email)  # Alice [email protected]

The validate_email static method is pure: it takes a string and returns a boolean. Using re.fullmatch (rather than re.match with explicit anchors) expresses the intent more directly — the entire string must match, not just a prefix. Inside __init__, the call is written as self.validate_email(email) rather than hardcoding User.validate_email(email). Both work identically on a direct User instantiation, but the self form routes through the descriptor and remains correct if a subclass overrides validate_email. The from_string class method needs cls because it creates a new instance. This combination -- static methods for validation, class methods for construction -- is a clean separation of concerns.

The Inheritance Test

The place where @classmethod and @staticmethod diverge in a way that matters for real architecture is inheritance. A class method always knows which class called it. A static method does not.

class Serializer:
    file_extension: str = ".txt"

    def __init__(self, data: str) -> None:
        self.data = data

    @classmethod
    def create_empty(cls) -> "Serializer":
        """Factory method that respects subclass identity."""
        print(f"Creating instance of {cls.__name__}")
        return cls("")

    @staticmethod
    def default_extension() -> str:
        """Static method -- no awareness of which class called it."""
        return ".txt"  # always returns parent's value

class JSONSerializer(Serializer):
    file_extension = ".json"

class XMLSerializer(Serializer):
    file_extension = ".xml"

# Class method polymorphism: cls is the subclass
json_obj = JSONSerializer.create_empty()
# Creating instance of JSONSerializer
print(type(json_obj))  # <class 'JSONSerializer'>

xml_obj = XMLSerializer.create_empty()
# Creating instance of XMLSerializer
print(type(xml_obj))   # <class 'XMLSerializer'>

# Static method: no polymorphism
print(JSONSerializer.default_extension())  # .txt (not .json!)
print(XMLSerializer.default_extension())   # .txt (not .xml!)

The create_empty class method produces the correct subclass type because cls resolves to JSONSerializer or XMLSerializer depending on which class called the method. The default_extension static method returns ".txt" regardless, because it has no mechanism to know which class invoked it. If the static method needed to return the subclass-specific extension, it would need to be converted to a class method that reads cls.file_extension.

Pro Tip

If you find yourself hardcoding the class name inside a @staticmethod (like return Serializer.file_extension), that is a signal the method should be a @classmethod instead. Using cls.file_extension keeps the method polymorphic across subclasses.

Check Your Understanding
Question 1 of 4

A subclass inherits a factory method defined with @classmethod that calls cls(). When the subclass calls this factory, what does cls refer to?

You have a method that converts a temperature in Celsius to Fahrenheit. It needs no access to any instance or class data. Which decorator should you use?

You access Dog.create() where create is a @staticmethod defined on the parent class Animal. What does Python return from the descriptor's __get__?

An instance stores an attribute named validate in its __dict__, and the class also defines validate as a @staticmethod. When you access my_obj.validate, which one wins?

@classmethod and Multiple Inheritance

When a class inherits from multiple parents, Python's Method Resolution Order (MRO) determines which version of a method gets called. @classmethod participates in MRO exactly as instance methods do — the lookup follows the MRO chain and the first match wins. The key difference from instance methods is that cls always refers to the class through which the method was originally called, regardless of where in the MRO the method was actually found.

class Timestamped:
    @classmethod
    def source(cls) -> str:
        return f"Timestamped (called on {cls.__name__})"

class Audited:
    @classmethod
    def source(cls) -> str:
        return f"Audited (called on {cls.__name__})"

class Record(Timestamped, Audited):
    pass

# MRO: Record -> Timestamped -> Audited -> object
print(Record.__mro__)
# (<class 'Record'>, <class 'Timestamped'>, <class 'Audited'>, <class 'object'>)

# Timestamped.source wins (first in MRO), but cls is Record
print(Record.source())
# Timestamped (called on Record)

# Calling directly on a parent bypasses MRO for that lookup,
# but cls is still bound to whatever class you called through
print(Audited.source())
# Audited (called on Audited)

A common trap in multiple inheritance with @classmethod is defining a factory in a mixin that creates instances using cls(), then mixing that into a class whose __init__ signature differs from what the factory expects. Because cls resolves to the concrete class at call time, the factory will attempt to call the concrete class's __init__, not the mixin's. Design mixin factory methods defensively: use **kwargs in both the factory and the concrete __init__, or document the required interface explicitly.

@staticmethod is immune to MRO concerns because it receives no class reference at all. The exact same function is returned regardless of which class in the hierarchy is used to access it, which is both its limitation (no polymorphism) and an advantage (predictable, no MRO-related surprises).

Calling Descriptors Directly

Because both decorators are non-data descriptors, you can call their __get__ method yourself to observe exactly what Python injects. This is not something you would do in production code, but it makes the binding behavior concrete and verifiable.

__func__, __wrapped__, and the Python 3.10 Callable Change

Both staticmethod and classmethod expose two introspection attributes that the article content in most tutorials skips entirely. __func__ returns the raw underlying function without any binding. __wrapped__ — added natively to both built-ins in Python 3.10 — points to the same function and is what tools like inspect.signature() and functools.wraps() follow when unwrapping decorators. (Python Built-in Functions, 3.10 changelog)

class Demo:
    @classmethod
    def cm(cls) -> type:
        return cls

    @staticmethod
    def sm(x: int) -> int:
        return x * 2

# __func__ retrieves the unwrapped function from both
print(Demo.__dict__['cm'].__func__)   # <function Demo.cm at 0x...>
print(Demo.__dict__['sm'].__func__)   # <function Demo.sm at 0x...>

# __wrapped__ (Python 3.10+) points to the same underlying function
print(Demo.__dict__['cm'].__wrapped__)  # <function Demo.cm at 0x...>
print(Demo.__dict__['sm'].__wrapped__)  # <function Demo.sm at 0x...>

# inspect.signature() uses __wrapped__ to look through the decorator
import inspect
print(inspect.signature(Demo.sm))  # (x: int) -> int

Python 3.10 also made staticmethod descriptors directly callable from within the class body — a behavior change that resolves a long-standing ergonomic problem. Before 3.10, accessing a @staticmethod by name inside the class body gave you the raw staticmethod object, which was not callable. Calling it raised TypeError: 'staticmethod' object is not callable. From 3.10 onward, the descriptor itself implements __call__, so you can invoke it directly. (CPython issue bpo-43682, contributed by Victor Stinner)

# Python 3.9 and earlier: staticmethod was NOT callable inside the class body
# class A:
#     @staticmethod
#     def helper(x): return x * 2
#     result = helper(5)  # TypeError: 'staticmethod' object is not callable

# Python 3.10+: staticmethod IS callable inside the class body
class A:
    @staticmethod
    def helper(x: int) -> int:
        return x * 2
    result = helper(5)   # Works: result == 10

print(A.result)  # 10
Note: classmethod is still not directly callable

The 3.10 callable change applied only to staticmethod. A raw classmethod descriptor object is still not directly callable. Calling classmethod(f)() without going through attribute access on a class raises a TypeError. This asymmetry exists because there is no unambiguous way to determine which class should be bound as cls when calling the descriptor directly.

class Probe:
    @classmethod
    def cm(cls) -> type:
        return cls

    @staticmethod
    def sm() -> str:
        return "static"

obj = Probe()

# --- @classmethod ---
# Access from the class: __get__(None, Probe) is called
cm_via_class = Probe.__dict__['cm'].__get__(None, Probe)
print(cm_via_class())        # <class '__main__.Probe'>

# Access from an instance: __get__(obj, type(obj)) is called
cm_via_instance = Probe.__dict__['cm'].__get__(obj, Probe)
print(cm_via_instance())     # <class '__main__.Probe'>  -- same result

# --- @staticmethod ---
# Access from the class: __get__(None, Probe) returns the raw function
sm_via_class = Probe.__dict__['sm'].__get__(None, Probe)
print(sm_via_class())        # static

# Access from an instance: __get__(obj, Probe) also returns the raw function
sm_via_instance = Probe.__dict__['sm'].__get__(obj, Probe)
print(sm_via_instance())     # static  -- no difference at all

The class method returns the same bound method whether accessed through the class or through an instance -- the instance is discarded and only its type matters. The static method returns the same raw function in both cases. The Python Descriptor HowTo Guide notes that for class methods, the calling format is identical whether the caller is an object or a class. (Python Descriptor HowTo Guide)

You can also verify at runtime that both are non-data descriptors by checking which dunder methods are defined:

class Check:
    @classmethod
    def cm(cls) -> None: pass

    @staticmethod
    def sm() -> None: pass

cm_descriptor = Check.__dict__['cm']
sm_descriptor = Check.__dict__['sm']

# Both have __get__ (making them descriptors)
print(hasattr(cm_descriptor, '__get__'))   # True
print(hasattr(sm_descriptor, '__get__'))   # True

# Neither has __set__ or __delete__ (making them non-data descriptors)
print(hasattr(cm_descriptor, '__set__'))   # False
print(hasattr(sm_descriptor, '__set__'))   # False

# Both built-in @classmethod and @staticmethod gained native __wrapped__
# in Python 3.10 -- no functools needed
print(type(cm_descriptor))  # <class 'classmethod'>
print(type(sm_descriptor))  # <class 'staticmethod'>

The distinction between data and non-data descriptor is not academic: because neither @staticmethod nor @classmethod defines __set__, an instance can shadow them by storing an attribute with the same name in its own __dict__. A regular property (a data descriptor) would prevent that.

Why the descriptor mechanics matter in practice

Understanding that both decorators are non-data descriptors has three immediate consequences for your code. First, an instance can silently shadow a class method or static method if you accidentally assign an attribute with the same name — a bug that is hard to spot because the method appears to disappear rather than raise an error. Second, introspection tools like inspect.signature(), help(), and IDEs all reach through the descriptor to the underlying function via __wrapped__, so your decorators stay transparent to tooling. Third, understanding __get__ tells you exactly why calling Dog.create_from_string() and my_dog.create_from_string() both work and both return a Dog — the descriptor discards the instance and binds the class either way.

When to Use Which

How to Choose: Decision Flowchart

Use this flowchart to trace any method decision. Start at the top and follow the path that matches your situation.

Criteria @classmethod @staticmethod
First argument cls (the class) None (no implicit argument)
Can access class attributes Yes, via cls No (only via hardcoded class name)
Can access instance attributes No No
Can create instances Yes, via cls(...) Only via hardcoded class name
Respects subclass inheritance Yes (cls is the subclass) No
Typical use case Factory methods, class state operations Utility functions, validation helpers
Can modify class state Yes No (without hardcoding)
Testability Needs class context Fully independent, like a free function
@classmethodcls (the class)
@staticmethodNone (no implicit argument)
@classmethodYes, via cls
@staticmethodNo (only via hardcoded class name)
@classmethodNo
@staticmethodNo
@classmethodYes, via cls(...)
@staticmethodOnly via hardcoded class name
@classmethodYes (cls is the subclass)
@staticmethodNo
@classmethodFactory methods, class state operations
@staticmethodUtility functions, validation helpers
@classmethodYes
@staticmethodNo (without hardcoding)
@classmethodNeeds class context
@staticmethodFully independent, like a free function
Performance: Is There a Difference?

@staticmethod has marginally lower call overhead than @classmethod because it skips the binding step entirely — the descriptor returns the raw function with no closure or method wrapping. @classmethod must construct a bound method that holds a reference to cls. In microbenchmarks the gap is typically in the range of tens of nanoseconds per call. In all practical applications this difference is irrelevant. Choose the decorator that correctly models the method's relationship to the class. If you have a hot path where the call overhead actually matters, a module-level function is faster than either decorator — but measure with timeit on your own hardware before optimising.

The decision rule is straightforward. If the method needs to know which class it belongs to -- because it creates instances, reads class attributes, or should behave differently in subclasses -- use @classmethod. If the method is a pure function that just happens to be logically related to the class, use @staticmethod. If the method needs access to instance data, it should be a regular instance method with no decorator at all.

Warning

A common mistake is defining a @classmethod that never uses cls, or a @staticmethod that hardcodes the parent class name to access class attributes. If your @classmethod ignores cls, it should probably be a @staticmethod. If your @staticmethod references the class by name, it should probably be a @classmethod.

Behaviour With __slots__

Both @staticmethod and @classmethod work correctly in classes that use __slots__. Because neither decorator stores anything in instance __dict__ — they live in the class's __dict__ — the presence or absence of __slots__ does not affect them. There is one subtlety worth knowing: since __slots__ eliminates the instance __dict__, the shadowing behaviour described for non-data descriptors disappears. An instance of a slotted class cannot shadow a @staticmethod or @classmethod by storing a same-named attribute, because there is nowhere to store it.

class Point:
    __slots__ = ("x", "y")

    def __init__(self, x: float, y: float) -> None:
        self.x = x
        self.y = y

    @classmethod
    def origin(cls) -> "Point":
        return cls(0.0, 0.0)

    @staticmethod
    def from_tuple(coords: tuple) -> "Point":
        return Point(coords[0], coords[1])

p = Point.origin()
print(p.x, p.y)  # 0.0 0.0

# Attempting to shadow via instance dict is impossible with __slots__
try:
    p.origin = lambda: None  # AttributeError: 'Point' has no attribute 'origin'
except AttributeError as e:
    print(e)  # confirms shadowing is blocked by __slots__

Type Checking: mypy and pyright

Both mypy and pyright understand @staticmethod and @classmethod natively and enforce their contracts. A few patterns that look correct at runtime will fail static analysis:

from __future__ import annotations
from typing import Self  # Python 3.11+ for Self type

class Shape:
    def __init__(self, color: str) -> None:
        self.color = color

    # CORRECT: return type uses Self so subclasses type-check correctly
    @classmethod
    def default(cls) -> Self:
        return cls("red")

    # CORRECT: @staticmethod with full type annotations
    @staticmethod
    def is_valid_color(color: str) -> bool:
        return color in {"red", "green", "blue", "black", "white"}

class Circle(Shape):
    def __init__(self, color: str, radius: float) -> None:
        super().__init__(color)
        self.radius = radius

    # CORRECT: override classmethod -- mypy/pyright accept this
    @classmethod
    def default(cls) -> Self:
        return cls("blue", 1.0)

# Using the legacy return type annotation triggers mypy errors on subclasses:
# def default(cls) -> "Shape": ...  # mypy: Incompatible return type in subclass

The critical typing rule: if a @classmethod factory returns cls(...), annotate the return type as Self (from typing in Python 3.11+, or typing_extensions for earlier versions), not as the base class name. Annotating it as "Shape" tells type checkers the method always returns a Shape, which causes them to report an error when a subclass overrides it to return a Circle. Self correctly expresses "returns whatever class this was called on." Type checkers also flag any attempt to access cls or self inside a @staticmethod as an error, which serves as a useful linter signal that you have used the wrong decorator.

Combining Both in a Real Class

A well-designed class uses all three method types in their appropriate roles:

import json
from typing import Any

class Config:
    """Application configuration with multiple loading strategies."""

    _defaults: dict[str, Any] = {"debug": False, "log_level": "INFO", "timeout": 30}

    def __init__(self, settings: dict[str, Any]) -> None:
        self.settings: dict[str, Any] = {**self._defaults, **settings}

    # Instance method: operates on this specific config
    def get(self, key: str, default: Any = None) -> Any:
        return self.settings.get(key, default)

    # Class method: factory that loads from a JSON file
    @classmethod
    def from_json_file(cls, filepath: str) -> "Config":
        """Load configuration from a JSON file."""
        with open(filepath) as f:
            data: dict[str, Any] = json.load(f)
        return cls(data)

    # Class method: factory that creates a debug config
    @classmethod
    def debug_config(cls) -> "Config":
        """Return a Config pre-set for debug mode."""
        return cls({"debug": True, "log_level": "DEBUG"})

    # Class method: read/modify class-level defaults
    @classmethod
    def set_default(cls, key: str, value: Any) -> None:
        """Update a shared default that applies to all future instances."""
        cls._defaults[key] = value

    # Static method: pure validation, no class/instance needed
    @staticmethod
    def validate_log_level(level: str) -> bool:
        """Return True if level is a recognised Python logging level."""
        valid = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
        return level.upper() in valid

    def __repr__(self) -> str:
        return f"Config({self.settings})"

# Static method: standalone validation
print(Config.validate_log_level("DEBUG"))   # True
print(Config.validate_log_level("VERBOSE")) # False

# Class method: modify shared defaults
Config.set_default("timeout", 60)

# Class method: alternative constructor
debug = Config.debug_config()
print(debug.get("debug"))      # True
print(debug.get("timeout"))    # 60 (picks up modified default)

# Instance method: access this config's data
print(debug.get("log_level"))  # DEBUG

Advanced Patterns: Solutions You Won't Find in Most Tutorials

The factory method and utility helper patterns are well-documented. These patterns are less discussed but appear constantly in serious Python codebases — frameworks, ORMs, plugin systems, and testable library design.

@classmethod as a Plugin Registry

One of the most powerful but least-documented uses of @classmethod is building self-registering plugin systems. Python's __init_subclass__ hook — itself implemented as a @classmethod on object — fires automatically whenever a class is subclassed. You can override it to register subclasses into a registry on the parent class, enabling a plugin architecture where adding a new subclass is all that is required to make it discoverable.

class Exporter:
    """Base class for all export formats. Subclasses self-register."""
    _registry: dict[str, type["Exporter"]] = {}

    def __init_subclass__(cls, format: str = "", **kwargs) -> None:
        super().__init_subclass__(**kwargs)
        if format:
            Exporter._registry[format] = cls

    @classmethod
    def for_format(cls, format: str) -> "Exporter":
        """Factory that returns the correct exporter by format name."""
        try:
            return cls._registry[format]()
        except KeyError:
            raise ValueError(f"No exporter registered for format: {format!r}")

    def export(self, data: dict) -> str:
        raise NotImplementedError

# Each subclass registers itself simply by declaring a format keyword
class JSONExporter(Exporter, format="json"):
    def export(self, data: dict) -> str:
        import json
        return json.dumps(data)

class CSVExporter(Exporter, format="csv"):
    def export(self, data: dict) -> str:
        return ",".join(data.keys()) + "
" + ",".join(str(v) for v in data.values())

# No import or manual registration needed — just use the factory
exporter = Exporter.for_format("json")
print(exporter.export({"name": "Alice", "age": 30}))
# {"name": "Alice", "age": 30}

print(Exporter._registry)
# {'json': <class 'JSONExporter'>, 'csv': <class 'CSVExporter'>}

This pattern is how Django's model system, SQLAlchemy's mapper registry, and many CLI frameworks auto-discover handlers. The __init_subclass__ hook fires at class definition time — no explicit registration call, no import side-effects needed. The @classmethod factory then provides a clean public API that decouples callers from concrete subclass names entirely.

__init_subclass__ is a @classmethod

__init_subclass__ is defined on object as an implicit @classmethod. You do not need the decorator when overriding it — Python applies it automatically. This makes it a built-in example of the language using class method mechanics to implement class-level lifecycle hooks. Understanding how @classmethod works under the descriptor protocol explains exactly why __init_subclass__ receives cls (the new subclass) rather than the class it was defined on.

@staticmethod as a Dependency Injection Seam

Because @staticmethod is a non-data descriptor, an instance can shadow it by storing a same-named callable in its __dict__. This is normally a hazard — but in test code it is a deliberate technique. You can replace a static method on a specific instance to inject a test double without subclassing or patching the class globally.

import datetime

class ReportGenerator:
    @staticmethod
    def current_date() -> datetime.date:
        return datetime.date.today()

    def generate(self) -> str:
        return f"Report for {self.current_date()}"

# In production: ReportGenerator().generate() returns today's date.

# In tests: shadow the static method on a specific INSTANCE
# without touching the class or using unittest.mock.patch.
gen = ReportGenerator()
gen.current_date = lambda: datetime.date(2025, 1, 1)   # instance shadows the descriptor
print(gen.generate())       # Report for 2025-01-01

# Other instances are unaffected
other = ReportGenerator()
print(other.generate())     # Report for <today's actual date>

This is a narrow but useful technique in unit tests where you want to control a side effect (the current time, a UUID generator, an external lookup) without setting up a full mock or altering global state. It works precisely because @staticmethod is a non-data descriptor — if it were a @property (a data descriptor), the instance assignment would raise an AttributeError. The technique should be used with care: it only isolates the specific instance, and tools like mypy will flag the assignment as a type error, since the instance attribute type differs from the declared static method signature.

Cooperative @classmethod Chains Across a Mixin Hierarchy

When multiple mixins in a cooperative inheritance chain each need to contribute to a factory, super() inside @classmethod enables each layer to add its own data before passing control up the chain. This is the class-method equivalent of cooperative __init__ with super(), and it is how complex framework base classes assemble configuration from multiple inheritance levels.

class BaseConfig:
    @classmethod
    def build(cls, **kwargs):
        """Root of the cooperative build chain."""
        return kwargs

class LoggingMixin:
    @classmethod
    def build(cls, **kwargs):
        kwargs.setdefault("log_level", "INFO")
        kwargs.setdefault("log_format", "json")
        return super().build(**kwargs)   # passes to next in MRO

class RetryMixin:
    @classmethod
    def build(cls, **kwargs):
        kwargs.setdefault("max_retries", 3)
        kwargs.setdefault("retry_backoff", 1.5)
        return super().build(**kwargs)

class APIClient(RetryMixin, LoggingMixin, BaseConfig):
    @classmethod
    def build(cls, **kwargs):
        kwargs.setdefault("timeout", 30)
        result = super().build(**kwargs)
        return cls(**result)   # finally construct the instance

    def __init__(self, timeout, log_level, log_format, max_retries, retry_backoff):
        self.timeout = timeout
        self.log_level = log_level
        self.max_retries = max_retries

# MRO: APIClient -> RetryMixin -> LoggingMixin -> BaseConfig -> object
# Each mixin contributes its defaults; only kwargs explicitly passed override them
client = APIClient.build(log_level="DEBUG", timeout=60)
print(client.timeout)      # 60 (explicitly passed)
print(client.log_level)    # DEBUG (explicitly passed, overrides mixin default)
print(client.max_retries)  # 3 (from RetryMixin default)

Each mixin's build method contributes its own defaults, then calls super().build(**kwargs) to pass control to the next class in the MRO. Because cls stays bound to APIClient throughout the entire chain, the final cls(**result) call constructs the concrete class, not any of the intermediate mixins. This pattern is common in Django class-based views, DRF serializers, and any framework that uses cooperative mixin composition.

@classmethod for Configuration Cascade Across Subclasses

A recurring architectural problem is building objects that should inherit configuration from parent classes but allow subclasses to selectively override individual settings. A @classmethod that walks the MRO to assemble the full configuration solves this more cleanly than either repeating defaults in every subclass or using a flat class attribute.

class View:
    """Base view with default configuration."""
    http_methods: list[str] = ["GET", "HEAD", "OPTIONS"]
    content_type: str = "text/html"
    cache_seconds: int = 0

    @classmethod
    def get_config(cls) -> dict:
        """Merge configuration from the full MRO, child settings win."""
        config = {}
        # Walk MRO in reverse so child class attributes override parents
        for klass in reversed(cls.__mro__):
            for attr in ("http_methods", "content_type", "cache_seconds"):
                if attr in klass.__dict__:       # only look at own attributes
                    config[attr] = klass.__dict__[attr]
        return config

class JSONView(View):
    content_type = "application/json"
    http_methods = ["GET", "POST"]

class CachedJSONView(JSONView):
    cache_seconds = 300   # only override what's needed

print(CachedJSONView.get_config())
# {
#   'http_methods': ['GET', 'POST'],      -- from JSONView
#   'content_type': 'application/json',  -- from JSONView
#   'cache_seconds': 300                 -- from CachedJSONView
# }

Using klass.__dict__ (rather than getattr(klass, attr)) is the crucial detail: it restricts the lookup to attributes explicitly defined on that specific class, preventing inherited values from overriding child-defined ones. This is a pattern used in Django's class-based view machinery and Flask-RESTful's resource system. The @classmethod is the only method type that can both receive cls for MRO traversal and be called before any instance exists.

Frequently Asked Questions

What is the difference between @staticmethod and @classmethod in Python?

A @classmethod receives the class itself as its first argument (conventionally named cls), allowing it to access and modify class-level attributes and create instances. A @staticmethod receives no implicit first argument at all -- neither the instance (self) nor the class (cls). It behaves like a regular function that happens to live inside a class's namespace. Both are implemented as non-data descriptors in CPython.

When should I use @classmethod instead of @staticmethod?

Use @classmethod when the method needs to access or modify the class, create instances via cls(), or behave polymorphically across subclasses. The cls parameter ensures the method always refers to the actual class it was called on, even when inherited by a subclass.

When should I use @staticmethod in Python?

Use @staticmethod for utility functions that logically belong to the class but do not need access to any class or instance data. Validation helpers, pure calculations, and format converters are common examples. Placing them inside the class provides namespacing and discoverability without coupling them to the class's state.

How does @classmethod behave with inheritance?

When a subclass inherits a @classmethod, the cls parameter receives the subclass, not the parent class. This means factory methods defined with @classmethod automatically produce instances of the subclass when called through it. A @staticmethod does not receive cls, so it has no awareness of which class in the hierarchy called it.

Can @staticmethod and @classmethod be called on instances?

Yes. Both can be called on either the class or an instance. When called on an instance, the instance is ignored for @staticmethod (no arguments are injected) and the instance's class is passed as cls for @classmethod. Calling them directly on the class is the conventional and clearer approach.

Are @staticmethod and @classmethod data descriptors or non-data descriptors?

Both are non-data descriptors. They implement only __get__() and define neither __set__() nor __delete__(). This means instances can shadow them by storing a same-named attribute in their own __dict__, unlike data descriptors such as property, which prevent that.

Can @classmethod wrap @property in Python 3.13?

No. The ability to wrap other descriptors such as @property with @classmethod was added in Python 3.9, deprecated in Python 3.11, and fully removed in Python 3.13. The Python 3.13 release notes confirm the core design was flawed and led to several problems. Code relying on this pattern must be rewritten using a custom descriptor or a regular @classmethod that returns the computed value. The __wrapped__ attribute (available since Python 3.10) is the documented workaround for passing through a classmethod without chaining descriptors.

Can a @staticmethod be called directly inside a class body?

Yes, from Python 3.10 onward. Before 3.10, a staticmethod object inside the class body was not callable — accessing it by name gave you the descriptor object itself, and calling it raised TypeError: 'staticmethod' object is not callable. Python 3.10 added __call__ to the staticmethod descriptor, making it directly invocable inside the class body. This change was contributed by Victor Stinner in CPython bpo-43682. Note that classmethod descriptors remain non-callable in this way.

What are __func__ and __wrapped__ on these decorators?

Both staticmethod and classmethod expose __func__, which returns the unwrapped underlying function. Both also gained a __wrapped__ attribute natively in Python 3.10. Tools like inspect.signature() follow __wrapped__ to look through the decorator and report the original function's signature. This is particularly useful when stacking decorators: the chain of __wrapped__ attributes lets introspection tools trace back to the original callable.

Does super() work inside a @classmethod?

Yes. super() inside a @classmethod returns a proxy that delegates to the next class in the MRO, but cls remains bound to the class the method was originally called on. This means super().some_classmethod() finds the parent class's implementation, but cls.__name__ still reflects the subclass — the subclass identity is preserved through the entire call. This is the foundation of cooperative factory hierarchies in Python.

Do @staticmethod and @classmethod work with __slots__?

Yes, fully. Both decorators live in the class __dict__, not in instance storage, so __slots__ has no effect on them. There is a useful side effect: __slots__ removes the instance __dict__, which means instances of slotted classes cannot shadow a @staticmethod or @classmethod by storing a same-named attribute. The non-data descriptor shadowing vulnerability that applies to regular classes does not apply to slotted ones.

How should a @classmethod factory be typed for mypy and pyright?

Use Self from typing (Python 3.11+) or typing_extensions as the return type annotation, not the base class name. Annotating with the base class name tells type checkers the factory always returns that exact type, which causes them to flag subclass overrides as incompatible. Self expresses "returns whatever class this was called on," which is precisely what cls(...) does at runtime. Type checkers also treat any attempt to use cls or self inside a @staticmethod as an error, which is a useful signal that the wrong decorator was chosen.

Is @staticmethod faster than @classmethod?

Marginally, yes — but not in any way that matters for application code. @staticmethod returns the raw function with no binding; @classmethod must construct a bound method holding a reference to cls. The difference is typically in the range of tens of nanoseconds per call. The only scenario where this gap is worth considering is a tight loop called millions of times per second, in which case a module-level function is faster than either decorator. For everything else, correctness and intent should drive the choice.

How does @classmethod behave under multiple inheritance?

It follows Python's MRO (Method Resolution Order) exactly as instance methods do — the first match in the inheritance chain wins. The important distinction is that cls always refers to the class the method was originally called on, not to the class where the method was defined. The main trap is a mixin factory that calls cls() and assumes a particular __init__ signature, only to find that cls resolves to a concrete class with a different signature. Defensive mixin factories accept **kwargs and document their required interface.

How can @classmethod be used to build a plugin registry?

By overriding __init_subclass__ — which Python invokes automatically whenever a class is subclassed — you can make each subclass register itself into a dict on the parent at class-definition time. A @classmethod factory on the parent then looks up and instantiates the correct subclass by key, completely decoupling callers from concrete class names. This is the pattern behind Django's model auto-discovery and SQLAlchemy's mapper registry. The full implementation is shown in the Advanced Patterns section.

Can @staticmethod be used for dependency injection in tests?

Yes. Because @staticmethod is a non-data descriptor (it defines no __set__), an instance can shadow it by assigning a callable to the same name in its __dict__. In test code, you can inject a test double — a fixed date, a mock UUID, a stubbed network call — on a single specific instance without subclassing, without unittest.mock.patch, and without affecting other instances. Type checkers will flag the assignment, so use it as an intentional test-only technique with a comment.

Can a @staticmethod be replaced on a single instance for testing?

Yes — deliberately. Because @staticmethod is a non-data descriptor (it defines no __set__), assigning a callable to the same name on a specific instance shadows the class-level descriptor for that instance only. This lets you inject a test double — a fixed date, a mock UUID, a stubbed network call — without subclassing, without unittest.mock.patch, and without affecting any other instance. All other instances continue to use the original static method. Type checkers will flag the assignment, so treat it as a deliberate test-only pattern with a comment explaining the intent.

Key Takeaways

  1. @classmethod receives the class as cls; @staticmethod receives nothing. This is the entire mechanical difference. Every behavioral difference flows from this: class methods can access class state, create instances polymorphically, and behave differently across subclasses. Static methods cannot.
  2. Both are non-data descriptors. They implement only __get__(), not __set__() or __delete__(). This means an instance can shadow either one by storing a same-named entry in its __dict__. A data descriptor like property takes precedence and would not allow that.
  3. Use @classmethod for factory methods. Because cls resolves to the actual class that was called (not necessarily the class where the method was defined), factory methods built with @classmethod automatically produce the correct subclass type when inherited.
  4. Use @staticmethod for pure utility functions. If a function does not need self or cls, making it a static method signals that it will not modify class or instance state. This makes the function easier to test (no class setup required) and communicates intent clearly.
  5. The canonical implementations emulate C-level types. The pure-Python equivalents in the Python documentation are labeled PyStaticMethod_Type() and PyClassMethod_Type() in Objects/funcobject.c. The StaticMethod.__get__ returns the raw function. The ClassMethod.__get__ uses a closure to bind the class as the first argument before returning the callable.
  6. If a @staticmethod references the class by name, convert it to a @classmethod. Hardcoding a class name inside a static method defeats the purpose of namespacing the method in a class and breaks when subclasses inherit the method.
  7. Do not wrap @property with @classmethod on Python 3.13+. That pattern was removed in Python 3.13 after being deprecated in 3.11. Code that relies on it will raise a TypeError at runtime. The documented workaround is the __wrapped__ attribute.
  8. Both expose __func__ and __wrapped__ (Python 3.10+). __func__ gives direct access to the underlying function. __wrapped__ is followed by inspect.signature() and decorator stacking tools. These attributes make both decorators fully transparent to introspection tooling.
  9. @staticmethod became callable inside the class body in Python 3.10. Before 3.10, accessing a @staticmethod by name inside the class body returned a non-callable descriptor object. From 3.10 onward, you can call it directly. @classmethod does not share this behavior and remains non-callable as a raw descriptor.
  10. super() works correctly inside @classmethod. It delegates method lookup to the next class in the MRO without changing what cls is. The subclass identity persists through super() calls, which is what makes cooperative factory hierarchies possible.
  11. Annotate @classmethod factories with Self, not the base class name. Self (Python 3.11+ / typing_extensions) correctly expresses that the return type matches the calling class. Using the base class name causes type checker errors when subclasses override the factory.
  12. Both work correctly with __slots__. Neither decorator stores anything in instance memory. As a bonus, __slots__ eliminates the instance __dict__, which removes the non-data descriptor shadowing vulnerability entirely.
  13. The performance difference is negligible. @staticmethod is marginally faster than @classmethod due to skipping the binding step. The gap is measured in nanoseconds. Use the correct decorator for the design; move to module-level functions only if profiling confirms a bottleneck.
  14. @classmethod + __init_subclass__ enables self-registering plugin architectures. Each subclass registers itself into the parent class's registry at definition time. A @classmethod factory on the parent provides a clean, decoupled API that instantiates the correct subclass by key.
  15. Cooperative @classmethod chains with super() compose well across mixin hierarchies. Each mixin contributes its own defaults to a shared kwargs dict before passing control up the MRO. Because cls stays bound to the concrete subclass throughout, the final cls() call produces the correct type.
  16. The non-data descriptor property of @staticmethod is useful in tests. Instance-level shadowing of a static method injects a test double without global patching. Use it deliberately, scope it to the test, and acknowledge the type-checker warning.

The choice between @staticmethod and @classmethod is not about preference. It is about whether the method needs to know which class it is operating on. If it does, cls is the answer. If it does not, the method has no business receiving it.