Class Methods, Static Methods and Properties
@classmethod, @staticmethod, and @property — what each is actually for, with real examples of when to reach for each.
Instance Methods vs Class Methods vs Static Methods
Every method you have written so far has been an instance method — it takes self as its first parameter and operates on one specific object. Python offers two other kinds of method, each with a different relationship to the class, and each declared with a decorator.
class Pizza:
def __init__(self, toppings):
self.toppings = toppings
def describe(self): # instance method — needs a specific pizza
return f"Pizza with {', '.join(self.toppings)}"
@classmethod
def margherita(cls): # class method — receives the CLASS, not an instance
return cls(["tomato", "mozzarella", "basil"])
@staticmethod
def is_valid_topping(topping): # static method — receives NEITHER
return topping.lower() not in {"pineapple"}
p = Pizza.margherita() # an alternate way to construct a Pizza, no instance needed yet
print(p.describe()) # "Pizza with tomato, mozzarella, basil"
print(Pizza.is_valid_topping("pineapple")) # FalseThe distinction is entirely about what the method automatically receives as its first argument: an instance method receives the specific object (self); a class method receives the class itself (cls) — useful when the logic needs to know about the class but not about any particular instance; a static method receives neither — it is really just a regular function that happens to live inside the class's namespace for organisational purposes.
The Single Most Common Real Use of @classmethod
Python only allows one __init__ per class, but real-world objects often need to be built from several different kinds of input — a raw dict from an API, a CSV row, a set of sensible defaults. @classmethod alternate constructors are the standard way to offer several named ways to build an object, all funnelling into the same __init__.
class User:
def __init__(self, username, email, is_admin=False):
self.username = username
self.email = email
self.is_admin = is_admin
@classmethod
def from_api_response(cls, data):
return cls(
username=data["user_name"],
email=data["contact_email"],
is_admin=data.get("role") == "admin",
)
@classmethod
def guest(cls):
return cls(username="guest", email="")
api_data = {"user_name": "asha", "contact_email": "asha@example.com", "role": "admin"}
u1 = User.from_api_response(api_data)
u2 = User.guest()The reason this is a classmethod and not just a plain standalone function: cls refers to whichever class it was actually called on, so if AdminUser subclasses User, calling AdminUser.from_api_response(data) correctly builds an AdminUser instance, not a plain User — a standalone function hardcoding User(...) could never do that.
When You Just Want a Function to Live Near Its Class
A static method is, functionally, just a plain function — it does not receive self or cls, and it cannot access or modify instance or class state directly. Its only real purpose is organisation: grouping a piece of logic that is conceptually related to the class, even though it does not need any of the class's data.
class Pizza:
@staticmethod
def is_valid_topping(topping):
banned = {"pineapple", "anchovy"}
return topping.lower() not in banned
# Callable from the class, without needing to build a Pizza first:
print(Pizza.is_valid_topping("mushroom")) # True
# Also callable from an instance — works, but doesn't use the instance at all
p = Pizza()
print(p.is_valid_topping("pineapple")) # Falseself or cls, it should almost always be a @staticmethod — writing it as a regular instance method that simply never uses self is a common, low-severity code smell that reviewers frequently flag, since it misleadingly implies the method depends on instance data when it does not.Computed Attributes That Look Like Plain Data
@property lets a method be accessed with plain attribute syntax — no parentheses — which is useful for exposing a computed value that reads naturally as data, while still running real code underneath.
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
r = Rectangle(4, 5)
print(r.get_area()) # 20 — has to be called as a methodclass Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
r = Rectangle(4, 5)
print(r.area) # 20 — no parentheses! Reads exactly like a data attribute
r.area = 30 # AttributeError — this property has no setter yet (see below)Setters and deleters — controlling assignment, not just reads
class Rectangle:
def __init__(self, width, height):
self._width = width
self.height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("width must be positive")
self._width = value
r = Rectangle(4, 5)
r.width = 10 # runs the setter — validated
r.width = -3 # ValueError: width must be positiveThis is exactly why @property matters beyond convenience: it lets a class start as plain public attributes, and later add validation or computed logic without breaking any code that already does rectangle.width = 10 — the calling code's syntax never has to change, only the class's internals do. This is a genuinely important design property (sometimes called "uniform access") that a language without properties, forcing every external caller to use get_width()/set_width() from day one, does not offer.
Refactoring a Public API Without Breaking Callers, at an Austin SaaS Company
A Subscription class originally stores monthly_price as a plain public attribute, used directly across dozens of call sites throughout the codebase. A new business rule arrives: the price must never be set below the plan's configured minimum, and every assignment needs to log a price-change event for billing audit purposes.
class Subscription:
def __init__(self, plan_minimum, monthly_price):
self.plan_minimum = plan_minimum
self.monthly_price = monthly_priceclass Subscription:
def __init__(self, plan_minimum, monthly_price):
self.plan_minimum = plan_minimum
self._monthly_price = monthly_price
@property
def monthly_price(self):
return self._monthly_price
@monthly_price.setter
def monthly_price(self, value):
if value < self.plan_minimum:
raise ValueError(f"Price cannot go below the plan minimum of ${self.plan_minimum}")
log_price_change(self._monthly_price, value)
self._monthly_price = valueWhy this mattered
Every one of the dozens of existing call sites reading or writing sub.monthly_price = ... continued working with zero changes — the property is indistinguishable from a plain attribute to calling code. Had the class instead switched to get_monthly_price()/set_monthly_price() methods (the pattern from languages without properties), every single call site across the codebase would have needed updating in the same PR — a far larger, riskier change for what is fundamentally an internal implementation detail.
Four Misconceptions About Class/Static Methods and Properties
5 Interview Questions — With Complete Answers
Method Decorator Mistakes Beginners Make Constantly
Errors You Will Hit With Method Decorators — And Exactly Why
🎯 Key Takeaways
- ✓Instance methods receive self (the specific object); classmethods receive cls (the class itself); staticmethods receive neither.
- ✓@classmethod is most commonly used for alternate constructors (User.from_api_response(data)) — and correctly respects subclasses, unlike a hardcoded standalone function.
- ✓@staticmethod is for a helper that is conceptually related to the class but never touches instance or class state — if a method never uses self or cls, it is a strong candidate for @staticmethod.
- ✓@property lets a method be accessed with plain attribute syntax, enabling validation or computed values without breaking existing calling code that reads/writes it like a normal attribute.
- ✓The backing attribute behind a property must have a different name than the property itself (typically a leading underscore) to avoid infinite recursion.
- ✓A property with no @x.setter is effectively read-only from outside the class, raising AttributeError on assignment.
What comes next
Module 24 closes out the Object-Oriented Python phase with abstract base classes — enforcing a contract across subclasses with Python's abc module.
Module 24 → Abstract Base Classes and InterfacesDiscussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.