Skip to content

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)

  1. The three method types at a glance
  2. Instance methods — the default
  3. @classmethod — factories and alternative constructors
  4. @staticmethod — utilities in the class namespace
  5. Inheritance behavior — the critical difference
  6. @classmethod for class-level state
  7. @classmethod in inheritance chains with super()
  8. Combining with other decorators
  9. Comparison with @property
  10. Real-world patterns
  11. 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 polymorphismcls 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!
For factory methods that must return the called class, use @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

See Method Resolution Order.


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:

class Config:
    _debug = False

    @classmethod
    @property
    def debug(cls) -> bool:
        return cls._debug

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

class C:
    @classmethod
    def f(cls): pass

# Roughly equivalent to:
# C.f = classmethod(C.f)

classmethod and staticmethod are descriptor objects. On access:

  • C.f → bound method with cls pre-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#

  1. What does cls refer to when SubClass.factory() calls an inherited @classmethod?
  2. Why can't @staticmethod factory methods be polymorphic across subclasses?
  3. When is a @classmethod better than __init__ alone?
  4. How does classmethod interact with super() in multiple inheritance?
  5. 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