~22 min · property, cached_property, staticmethod, classmethod, builtin
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
The four built-in decorators every Python developer uses
You'll write your own decorators occasionally. You'll use these four every week. @property, @cached_property, @staticmethod, @classmethod all live in builtins or stdlib, and each solves a specific class-design problem cleanly.
@property — attribute that's actually a function
You want users to write obj.area, not obj.area(), but you also want area to be computed (not stored). @property turns a method into a getter that runs when the attribute is accessed. You can add a setter and deleter too, with @area.setter and @area.deleter. The class looks like it has a normal attribute; the implementation does whatever you want under the hood.
@cached_property — compute once, then it's an attribute
Same usage shape as @property, but the function only runs once per instance. After the first access, the result is stored as a regular attribute on the instance. Subsequent accesses are plain attribute lookups — no method call. Perfect for expensive computations whose result doesn't change. New in 3.8 (was previously a third-party recipe).
@staticmethod — a function that lives in a class namespace
The method receives no self and no cls. It's just a function attached to the class for organizational reasons. Use it when a piece of code logically belongs with the class but doesn't need access to instance or class state. Modern style often prefers a module-level function for this, but @staticmethod is still common in stdlib and many codebases.
@classmethod — receives the class, not the instance
The method receives cls instead of self. Used most often for alternative constructors: User.from_dict(d) or Date.today(). The cls argument means subclasses get the right class automatically — SpecialUser.from_dict(d) returns a SpecialUser, not a User.
Principle: When you find yourself adding get_x() and set_x() methods to a class, stop and ask whether @property would express the intent better. The bias in idiomatic Python is toward attribute-shaped APIs over method-shaped ones for things that conceptually are properties of the object.
Code
@property — getter, setter, deleter·python
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value <= 0:
raise ValueError("radius must be positive")
self._radius = value
@property
def area(self): # computed, no setter needed
return 3.14159 * self._radius ** 2
c = Circle(5)
print(c.radius) # 5 — looks like an attribute
print(c.area) # 78.54 — computed on access
c.radius = 10 # uses the setter
print(c.area) # 314.16
try:
c.radius = -1 # setter validates
except ValueError as e:
print(e)
@cached_property — compute once·python
from functools import cached_property
import time
class Report:
def __init__(self, data):
self.data = data
@cached_property
def summary(self):
print("computing summary...")
time.sleep(0.5) # imagine an expensive operation
return f"sum={sum(self.data)}, avg={sum(self.data)/len(self.data)}"
r = Report([10, 20, 30, 40, 50])
print(r.summary) # 'computing summary...' then the result
print(r.summary) # NO recomputation — returns cached value
print(r.summary) # NO recomputation
# To force recomputation, delete the cached attribute
del r.summary
print(r.summary) # 'computing summary...' again
@staticmethod and @classmethod — alternative constructors·python
from datetime import date
class User:
def __init__(self, name, joined):
self.name = name
self.joined = joined
@classmethod
def from_dict(cls, d):
# Returns the right class for subclasses
return cls(name=d["name"], joined=d.get("joined", date.today()))
@classmethod
def from_csv_row(cls, row):
name, joined = row.split(",")
return cls(name.strip(), date.fromisoformat(joined.strip()))
@staticmethod
def is_valid_name(name):
# Doesn't touch self or cls — just utility logic
return isinstance(name, str) and 1 <= len(name) <= 50
u = User.from_dict({"name": "Pippa"})
print(u.name, u.joined)
print(User.is_valid_name("x")) # True
print(User.is_valid_name("")) # False
# Subclass — classmethod still returns the right type
class Admin(User):
pass
a = Admin.from_dict({"name": "Dad"})
print(type(a).__name__) # 'Admin' — not 'User'!
@property + caching — the manual way (when you want control)·python
class Stock:
def __init__(self, ticker, price):
self.ticker = ticker
self.price = price
self._fetched = None
@property
def cached_data(self):
if self._fetched is None:
print("fetching...")
self._fetched = {"ticker": self.ticker, "price": self.price}
return self._fetched
def refresh(self):
self._fetched = None # invalidate the cache
s = Stock("AAPL", 180)
print(s.cached_data) # 'fetching...' then dict
print(s.cached_data) # NO fetching
s.refresh()
print(s.cached_data) # 'fetching...' again — refresh worked
# cached_property is shorter for the common case;
# manual @property is right when you need invalidation logic
Build a class Temperature that stores a value internally in Kelvin (self._k) but exposes three properties: kelvin, celsius, fahrenheit. Each property should be readable AND writable — setting temperature.celsius = 25 should update the internal Kelvin to 298.15. Validate in the setter that the new value would not produce a negative Kelvin (raise ValueError if it would). Add a @cached_property for description that returns a string like 'water freezes at 273.15K'. Test all paths.
Progress
Progress is local-only — sign in to sync across devices.