Class Methods and Static Methods#
Python classes support three method flavors: instance methods (default), @classmethod, and @staticmethod. Each receives different implicit arguments and serves a distinct purpose. Confusing them is a common interview mistake — especially around alternative constructors and inheritance.
This page deepens the overview in Introdicion to OOPs and Classes and OOP Basics.
How to use this page
Read after basic OOP. Related: Inheritance and Multiple Inheritance, @property Decorators, Decorators.
At a glance
| Track | Python Advanced → Object Oriented Programming |
| Sections | 11 major topics |
| Outline | Use the right-hand TOC to jump |
Topics: The three method types at a glance · Instance methods — the default · @classmethod — factories and alternative constructors · @staticmethod — utilities in the class namespace · Inheritance behavior — the critical difference · @classmethod for class-level state · @classmethod in inheritance chains with super() · Combining with other decorators · … (+3 more)
- The three method types at a glance
- Instance methods — the default
@classmethod— factories and alternative constructors@staticmethod— utilities in the class namespace- Inheritance behavior — the critical difference
@classmethodfor class-level state@classmethodin inheritance chains withsuper()- Combining with other decorators
- Comparison with
@property - Real-world patterns
- How decorators transform methods (CPython view)
The three method types at a glance#
class Demo:
class_var = "shared"
def instance_method(self):
"""Receives the instance."""
return self
@classmethod
def class_method(cls):
"""Receives the class (or subclass)."""
return cls
@staticmethod
def static_method():
"""Receives nothing implicit."""
return "utility"
| Type | First argument | Access instance? | Access class? | Typical use |
|---|---|---|---|---|
| Instance method | self |
Yes | Via self.__class__ or type(self) |
Read/write instance state |
@classmethod |
cls |
No (unless you create one) | Yes | Factories, alternative constructors |
@staticmethod |
none | No | No | Namespace-organized utility |
Instance methods — the default#
class BankAccount:
interest_rate = 0.02
def __init__(self, balance: float = 0):
self.balance = balance
def deposit(self, amount: float) -> None:
if amount <= 0:
raise ValueError("amount must be positive")
self.balance += amount
def apply_interest(self) -> None:
self.balance *= 1 + self.__class__.interest_rate
self is conventional — Python passes the instance automatically. You can name it anything, but don't.
@classmethod — factories and alternative constructors#
The class object is passed as the first argument — cls refers to the class that was called, not necessarily the class where the method is defined.
class Date:
def __init__(self, year: int, month: int, day: int):
self.year = year
self.month = month
self.day = day
@classmethod
def from_iso(cls, s: str) -> "Date":
year, month, day = map(int, s.split("-"))
return cls(year, month, day)
@classmethod
def today(cls) -> "Date":
import datetime
t = datetime.date.today()
return cls(t.year, t.month, t.day)
def __repr__(self) -> str:
return f"Date({self.year}, {self.month}, {self.day})"
Date.from_iso("2026-07-07") # Date(2026, 7, 7)
Date.today()
Why @classmethod beats a standalone function for constructors#
class Employee:
def __init__(self, name: str, salary: float):
self.name = name
self.salary = salary
@classmethod
def from_csv_row(cls, row: str) -> "Employee":
name, salary = row.split(",")
return cls(name, float(salary))
class Manager(Employee):
def __init__(self, name: str, salary: float, team_size: int):
super().__init__(name, salary)
self.team_size = team_size
@classmethod
def from_csv_row(cls, row: str) -> "Manager":
name, salary, team_size = row.split(",")
return cls(name, float(salary), int(team_size))
Manager.from_csv_row("Alice,120000,5") # returns Manager, not Employee
A module-level from_csv_row(data, cls=Employee) could work, but @classmethod keeps construction logic colocated and respects subclass polymorphism — cls is Manager when called on Manager.
@staticmethod — utilities in the class namespace#
class MathUtils:
@staticmethod
def is_prime(n: int) -> bool:
if n < 2:
return False
for i in range(2, int(n ** 0.5) + 1):
if n % i == 0:
return False
return True
@staticmethod
def clamp(value: float, low: float, high: float) -> float:
return max(low, min(high, value))
MathUtils.is_prime(17) # True — call on class
MathUtils.clamp(15, 0, 10) # 10
Static methods do not receive self or cls. They behave like plain functions but live in the class namespace for organization.
When a module-level function is better#
If the utility has no logical connection to the class, a module-level function is cleaner:
# Prefer this if used across many unrelated classes
def is_valid_email(s: str) -> bool:
return "@" in s
Use @staticmethod when the function is conceptually part of the class API (validators, parsers, constants helpers).
Inheritance behavior — the critical difference#
class Base:
@classmethod
def who(cls) -> str:
return f"classmethod: {cls.__name__}"
@staticmethod
def who_static() -> str:
return "staticmethod: Base"
class Derived(Base):
pass
Derived.who() # "classmethod: Derived" — cls is Derived
Derived.who_static() # "staticmethod: Base" — no cls, hardcoded name
| Method type | Overridable polymorphically? | cls/self binding |
|---|---|---|
@classmethod |
Yes — cls is the called class |
Dynamic |
@staticmethod |
Can override, but no automatic binding | None |
| Instance method | Yes — self is the instance |
Dynamic |
Interview trap — staticmethod and polymorphism
class Base:
@staticmethod
def create():
return Base()
class Derived(Base):
pass
Derived.create() # returns Base, NOT Derived!
@classmethod with return cls(...).
@classmethod for class-level state#
class ConnectionPool:
_max_size: int = 10
_active: int = 0
@classmethod
def configure(cls, max_size: int) -> None:
cls._max_size = max_size
@classmethod
def acquire(cls) -> bool:
if cls._active >= cls._max_size:
return False
cls._active += 1
return True
@classmethod
def release(cls) -> None:
cls._active = max(0, cls._active - 1)
Note: mutating cls._max_size on a subclass creates a subclass attribute if not present on subclass — understand attribute lookup. See Encapsulation and Access Modifiers.
@classmethod in inheritance chains with super()#
Cooperative class methods in multiple inheritance:
class A:
@classmethod
def setup(cls) -> list[str]:
return ["A"]
class B(A):
@classmethod
def setup(cls) -> list[str]:
return super().setup() + ["B"]
class C(A):
@classmethod
def setup(cls) -> list[str]:
return super().setup() + ["C"]
class D(B, C):
@classmethod
def setup(cls) -> list[str]:
return super().setup() + ["D"]
D.setup() # ['A', 'C', 'B', 'D'] — follows MRO: D → B → C → A
Combining with other decorators#
@classmethod + @abstractmethod#
from abc import ABC, abstractmethod
class Widget(ABC):
@classmethod
@abstractmethod
def create_default(cls) -> "Widget": ...
@classmethod + @property (Python 3.9+)#
Class-level properties are possible but uncommon:
Prefer simple class attributes unless computation is needed.
Comparison with @property#
| Feature | @property |
@classmethod |
@staticmethod |
|---|---|---|---|
| Called on | Instance | Class or instance | Class or instance |
| Implicit arg | self (getter) |
cls |
None |
| Primary use | Managed attribute | Factory / class config | Utility function |
| Inheritance | Override getter/setter | cls reflects called class |
No cls binding |
class User:
_count = 0
def __init__(self, name: str):
self.name = name
type(self)._count += 1
@classmethod
def count(cls) -> int:
return cls._count
@property
def display(self) -> str:
return f"User: {self.name}"
Real-world patterns#
Named constructor (enum-style)#
class Color:
def __init__(self, r: int, g: int, b: int):
self.r, self.g, self.b = r, g, b
@classmethod
def from_hex(cls, hex_str: str) -> "Color":
hex_str = hex_str.lstrip("#")
r, g, b = (int(hex_str[i:i+2], 16) for i in (0, 2, 4))
return cls(r, g, b)
@classmethod
def red(cls) -> "Color":
return cls(255, 0, 0)
Color.from_hex("#FF5733")
Color.red()
Validation staticmethod#
class Email:
def __init__(self, address: str):
if not Email.is_valid(address):
raise ValueError("invalid email")
self.address = address
@staticmethod
def is_valid(address: str) -> bool:
return "@" in address and "." in address.split("@")[-1]
Singleton via __new__ + classmethod (rare)#
class Database:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
@classmethod
def get_instance(cls) -> "Database":
return cls()
Singletons are controversial — often a module-level object suffices.
How decorators transform methods (CPython view)#
classmethod and staticmethod are descriptor objects. On access:
C.f→ bound method withclspre-filled (classmethod)C().f→ same for classmethod (cls still from type)staticmethod→ returns the underlying function unchanged
Interview traps (quick reference)#
| Trap | What goes wrong | Safe approach |
|---|---|---|
Factory as @staticmethod |
Returns wrong type in subclasses | Use @classmethod with return cls(...) |
Using @classmethod when you need self |
Can't access instance state | Use instance method |
| Using instance method as factory | Awkward cls access via self.__class__ |
Use @classmethod |
Forgetting cls first param |
TypeError on call |
First param must be cls |
| Classmethod modifying shared mutable class attr | Subclass shares or shadows unexpectedly | Document class vs instance attrs |
| Overusing staticmethod | Clutters class API | Module function if unrelated |
Mental model checklist#
- What does
clsrefer to whenSubClass.factory()calls an inherited@classmethod? - Why can't
@staticmethodfactory methods be polymorphic across subclasses? - When is a
@classmethodbetter than__init__alone? - How does
classmethodinteract withsuper()in multiple inheritance? - When should a utility be a module function instead of
@staticmethod?
What's next#
| Topic | Page |
|---|---|
| Inheritance and factories | Inheritance and Multiple Inheritance |
| MRO and super() | Method Resolution Order |
| Managed attributes | @property Decorators |
| Basics OOP | Introdicion to OOPs |