Class Methods and Static Methods

An instance method receives the object, a class method receives the class, and a static method receives neither. Each exists for a different job.

The three kinds

class Example:
    def instance_method(self):
        return f"instance method, self is {self}"

    @classmethod
    def class_method(cls):
        return f"class method, cls is {cls.__name__}"

    @staticmethod
    def static_method():
        return "static method, no automatic first argument"


e = Example()
print(e.instance_method())
print(e.class_method())          # works from an instance
print(Example.class_method())    # and from the class
print(Example.static_method())
First argumentAccess toCall on
Instance methodselfInstance and class dataAn instance
Class methodclsClass data onlyEither
Static methodnoneNeither, automaticallyEither

Class methods as alternative constructors

This is the single most valuable use. __init__ takes one set of arguments; a class method can build an instance from something else entirely.

from datetime import date


class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    @classmethod
    def from_birth_year(cls, name, year):
        return cls(name, date.today().year - year)

    @classmethod
    def from_string(cls, text):
        name, age = text.split(",")
        return cls(name.strip(), int(age))

    @classmethod
    def from_dict(cls, data):
        return cls(data["name"], data["age"])

    def __repr__(self):
        return f"Person({self.name!r}, {self.age})"


print(Person("Meera", 27))
print(Person.from_birth_year("Arun", 1995))
print(Person.from_string("Sara, 24"))
print(Person.from_dict({"name": "Ravi", "age": 31}))

Each one reads clearly at the call site. The alternative - a single __init__ with five optional parameters and a chain of if statements - is much harder to use and to document.

Why cls rather than the class name

class Base:
    @classmethod
    def create(cls):
        return cls()             # builds whatever class it was called on

    @classmethod
    def create_wrong(cls):
        return Base()            # always builds a Base


class Child(Base):
    pass


print(type(Child.create()))          # <class 'Child'>   correct
print(type(Child.create_wrong()))    # <class 'Base'>    wrong

Using cls makes alternative constructors inherit correctly. Hard coding the class name breaks every subclass.

Class methods for shared state

class Session:
    _active = 0
    _total = 0

    def __init__(self, user):
        self.user = user
        Session._active += 1
        Session._total += 1

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

    @classmethod
    def active_count(cls):
        return cls._active

    @classmethod
    def statistics(cls):
        return {"active": cls._active, "total": cls._total}

    @classmethod
    def reset(cls):
        cls._active = 0
        cls._total = 0


a = Session("meera")
b = Session("arun")
a.close()

print(Session.active_count())      # 1
print(Session.statistics())        # {'active': 1, 'total': 2}

Class methods for configuration

class Connection:
    default_timeout = 30

    def __init__(self, host, port, timeout=None):
        self.host = host
        self.port = port
        self.timeout = timeout if timeout is not None else self.default_timeout

    @classmethod
    def set_default_timeout(cls, seconds):
        if seconds <= 0:
            raise ValueError("timeout must be positive")
        cls.default_timeout = seconds

    def __repr__(self):
        return f"Connection({self.host}:{self.port}, timeout={self.timeout})"


print(Connection("localhost", 8080))
Connection.set_default_timeout(5)
print(Connection("localhost", 8080))

Static methods

class DateHelper:
    @staticmethod
    def is_leap_year(year):
        return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)

    @staticmethod
    def days_in_month(year, month):
        if month == 2:
            return 29 if DateHelper.is_leap_year(year) else 28
        return 30 if month in (4, 6, 9, 11) else 31


print(DateHelper.is_leap_year(2024))        # True
print(DateHelper.days_in_month(2024, 2))    # 29

A static method is a plain function that lives inside a class for organisational reasons. It uses neither self nor cls.

When a static method is justified

class Order:
    def __init__(self, items):
        self.items = items

    @staticmethod
    def _apply_tax(amount, rate=0.18):
        """A pure helper, meaningful only to Order but needing no state."""
        return round(amount * (1 + rate), 2)

    def total(self):
        subtotal = sum(price * qty for price, qty in self.items)
        return self._apply_tax(subtotal)


print(Order([(100, 2), (50, 1)]).total())
If a static method is not conceptually tied to the class, make it a module level function instead. Python has no requirement that every function live in a class, and a free function is easier to import and test.

Validators as static methods

class User:
    def __init__(self, username, email):
        if not self.is_valid_username(username):
            raise ValueError(f"invalid username: {username!r}")
        if not self.is_valid_email(email):
            raise ValueError(f"invalid email: {email!r}")
        self.username = username
        self.email = email

    @staticmethod
    def is_valid_username(value):
        return (
            isinstance(value, str)
            and 3 <= len(value) <= 20
            and value.replace("_", "").isalnum()
        )

    @staticmethod
    def is_valid_email(value):
        return isinstance(value, str) and value.count("@") == 1 and "." in value.split("@")[1]

    def __repr__(self):
        return f"User({self.username!r})"


print(User("meera_n", "m@example.com"))
print(User.is_valid_username("ab"))          # False, usable without an instance

for bad in [("ab", "m@example.com"), ("meera", "not-an-email")]:
    try:
        User(*bad)
    except ValueError as error:
        print(error)

Validators as static methods can be called before constructing an object, which is exactly what a form or an API layer needs.

Choosing between them

class Example:
    registry = []

    def uses_instance(self):
        return self.name                 # needs self -> instance method

    @classmethod
    def uses_class(cls):
        return cls.registry              # needs cls -> class method

    @staticmethod
    def uses_neither(a, b):
        return a + b                     # needs neither -> static method
  1. Does it need instance data? Instance method.
  2. Does it need the class - to build an instance, or to read class state? Class method.
  3. Neither, but it belongs with this class conceptually? Static method.
  4. Neither, and it does not really belong? A module level function.

A complete example

from datetime import date


class Invoice:
    TAX_RATE = 0.18
    _next_number = 1000
    _issued = []

    def __init__(self, customer, lines, issued_on=None):
        self.number = Invoice._take_number()
        self.customer = customer
        self.lines = lines
        self.issued_on = issued_on or date.today()
        Invoice._issued.append(self)

    # --- instance behaviour -------------------------------------------------
    def subtotal(self):
        return sum(self.line_total(p, q) for p, q in self.lines)

    def total(self):
        return round(self.subtotal() * (1 + self.TAX_RATE), 2)

    # --- class level --------------------------------------------------------
    @classmethod
    def _take_number(cls):
        number = cls._next_number
        cls._next_number += 1
        return number

    @classmethod
    def from_dict(cls, data):
        return cls(data["customer"], [tuple(line) for line in data["lines"]])

    @classmethod
    def issued_count(cls):
        return len(cls._issued)

    @classmethod
    def revenue(cls):
        return round(sum(inv.total() for inv in cls._issued), 2)

    # --- pure helpers -------------------------------------------------------
    @staticmethod
    def line_total(price, quantity):
        return round(price * quantity, 2)

    def __repr__(self):
        return f"Invoice(#{self.number}, {self.customer!r}, {self.total():,.2f})"


a = Invoice("Meera", [(100, 2), (50, 3)])
b = Invoice.from_dict({"customer": "Arun", "lines": [[250, 1]]})

print(a)
print(b)
print(Invoice.issued_count(), Invoice.revenue())
print(Invoice.line_total(19.99, 3))     # usable with no invoice at all

Common mistakes

  • Forgetting the @classmethod or @staticmethod decorator, so cls silently receives an instance.
  • Hard coding the class name inside a class method instead of using cls.
  • Using a static method where a module level function belongs.
  • Modifying class state through self, which creates an instance attribute instead.
  • Trying to reach instance data from a class method.
  • Using a class method as a constructor but returning the wrong class.

Best practices

  • Use class methods for alternative constructors, and always build with cls(...).
  • Use class methods for anything that reads or updates class level state.
  • Use static methods for pure helpers that belong to the class conceptually.
  • Prefer a module level function when a static method has no real connection to the class.
  • Name alternative constructors from_something; it is a widely understood convention.

Practice

  1. Add three alternative constructors to a class: from a string, from a dictionary and from a file line.
  2. Track how many instances of a class have been created, using a class method to report it.
  3. Show what goes wrong when a class method hard codes its own class name and is then subclassed.
  4. Convert a static method into a module level function and explain when you would not.
  5. Write a class with all three method types and justify each choice in one sentence.

Conclusion

self for the object, cls for the class, nothing for a pure helper. Alternative constructors built with cls(...) are the highest value use of class methods, and they are what makes a class pleasant to build instances of.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Python notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.