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 methods as alternative constructors
- Why cls rather than the class name
- Class methods for shared state
- Class methods for configuration
- Static methods
- When a static method is justified
- Validators as static methods
- Choosing between them
- A complete example
- Common mistakes
- Best practices
- Practice
- Conclusion
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
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 argument | Access to | Call on | |
|---|---|---|---|
| Instance method | self | Instance and class data | An instance |
| Class method | cls | Class data only | Either |
| Static method | none | Neither, automatically | Either |
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'> wrongUsing 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)) # 29A 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- Does it need instance data? Instance method.
- Does it need the class - to build an instance, or to read class state? Class method.
- Neither, but it belongs with this class conceptually? Static method.
- 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 allCommon mistakes
- Forgetting the
@classmethodor@staticmethoddecorator, soclssilently 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
- Add three alternative constructors to a class: from a string, from a dictionary and from a file line.
- Track how many instances of a class have been created, using a class method to report it.
- Show what goes wrong when a class method hard codes its own class name and is then subclassed.
- Convert a static method into a module level function and explain when you would not.
- 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.