C.W.K.
Stream
Lesson 05 of 05 · published

Built-in Decorators — @property, @cached_property, @staticmethod, @classmethod

~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

External links

Exercise

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.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.