Difference Between @staticmethod and @classmethod in Python

Both @staticmethod and @classmethod define methods that can be called on a class without creating an instance. The difference is what information Python passes to the method when it is called. A @classmethod receives the class itself as its first argument. A @staticmethod receives nothing -- no class, no instance. That single difference cascades into different use cases, different behavior with inheritance, and different roles in class design. This article traces the distinction from its mechanical root through every practical consequence.

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):
        self.instance_var = value

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

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

    # Static method: receives nothing automatically
    @staticmethod
    def static_method(x, y):
        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.

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 descriptors -- objects that customize attribute access through a __get__ method. 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 strips away the binding mechanism entirely and returns the raw function:

# Simplified pure-Python equivalent of @staticmethod
class StaticMethod:
    def __init__(self, func):
        self.func = func

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

For a @classmethod, the descriptor binds the class to the function's first argument:

import functools

# Simplified pure-Python equivalent of @classmethod
class ClassMethod:
    def __init__(self, func):
        self.func = func

    def __get__(self, obj, objtype=None):
        if objtype is None:
            objtype = type(obj)
        # Return a partial that pre-fills cls
        return functools.partial(self.func, objtype)

This descriptor behavior is the mechanical heart of the difference. When you call Demo.class_method(), the descriptor intercepts the attribute access, binds Demo to the first argument, and returns a callable that, when invoked, calls the original function with cls=Demo. When you call Demo.static_method(3, 4), the descriptor returns the raw function with no binding at all.

@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, age):
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name, birth_year):
        """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):
        """Create an Employee from a dictionary."""
        return cls(data["name"], data["age"])

    def __repr__(self):
        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.

Accessing and Modifying Class State

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

class ConnectionPool:
    _max_connections = 10
    _active = 0

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

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

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

    def close(self):
        ConnectionPool._active -= 1

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

@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):
        return c * 9 / 5 + 32

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

    @staticmethod
    def celsius_to_kelvin(c):
        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, email):
        if not User.validate_email(email):
            raise ValueError(f"Invalid email: {email}")
        self.name = name
        self.email = email

    @staticmethod
    def validate_email(email):
        """Check whether a string looks like a valid email address."""
        pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$"
        return bool(re.match(pattern, email))

    @classmethod
    def from_string(cls, user_string):
        """Parse 'Name ' format."""
        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 ")
print(user.name, user.email)  # Alice [email protected]

The validate_email static method is pure: it takes a string and returns a boolean. It does not need cls or self. 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 = ".txt"

    def __init__(self, data):
        self.data = data

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

    @staticmethod
    def default_extension():
        """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))  # 

xml_obj = XMLSerializer.create_empty()
# Creating instance of XMLSerializer
print(type(xml_obj))   # 

# 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.

When to Use Which

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

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.

Combining Both in a Real Class

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

import json

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

    _defaults = {"debug": False, "log_level": "INFO", "timeout": 30}

    def __init__(self, settings):
        self.settings = {**self._defaults, **settings}

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

    # Class method: factory that loads from a JSON file
    @classmethod
    def from_json_file(cls, filepath):
        with open(filepath) as f:
            data = json.load(f)
        return cls(data)

    # Class method: factory that creates a debug config
    @classmethod
    def debug_config(cls):
        return cls({"debug": True, "log_level": "DEBUG"})

    # Class method: read/modify class-level defaults
    @classmethod
    def set_default(cls, key, value):
        cls._defaults[key] = value

    # Static method: pure validation, no class/instance needed
    @staticmethod
    def validate_log_level(level):
        valid = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
        return level.upper() in valid

    def __repr__(self):
        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

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. 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.
  3. 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.
  4. Both are implemented as descriptors. @staticmethod's __get__ returns the bare function. @classmethod's __get__ returns a partial application that binds the class to the first argument. Understanding this mechanism clarifies why class methods are polymorphic and static methods are not.
  5. 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.

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.