Mastery
Mastery/Python/D. Classes & OOP internals
T1 · high-leverage

@property, @staticmethod, @classmethod -- and the descriptor protocol underneath

All three are descriptors: objects with __get__/__set__ that live on the class, and intercept attribute access on instances. property is just a built-in descriptor class -- writing your own descriptor from scratch shows exactly what @property does for you, and explains why descriptors are looked up on the class (type(obj).__dict__), not the instance.

python
class LoggedAttr:
    """A minimal hand-written descriptor, doing by hand what @property automates."""
    def __set_name__(self, owner, name):
        self.name = "_" + name          # called automatically when the class body executes
    def __get__(self, obj, objtype=None):
        if obj is None:
            return self
        print(f"  (getting {self.name})")
        return getattr(obj, self.name, None)
    def __set__(self, obj, value):
        print(f"  (setting {self.name} = {value})")
        setattr(obj, self.name, value)

class Widget:
    color = LoggedAttr()          # descriptor instance, lives on the CLASS

    @property
    def size(self):
        return getattr(self, "_size", 0)
    @size.setter
    def size(self, v):
        self._size = v

w = Widget()
w.color = "red"       # triggers LoggedAttr.__set__
print(w.color)         # triggers LoggedAttr.__get__
w.size = 42            # triggers the property's setter
print(w.size)

print("property is a class attribute, an instance of the built-in descriptor type:", type(Widget.__dict__["size"]))

staticmethod and classmethod are descriptors too -- their __get__ controls what gets bound to the call. A staticmethod binds nothing (no implicit first argument); a classmethod binds the class, not an instance, which is exactly what makes alternative-constructor patterns like dict.fromkeys possible.

python
class Temperature:
    def __init__(self, celsius):
        self.celsius = celsius

    @classmethod
    def from_fahrenheit(cls, f):
        # `cls` is bound to the class -- works correctly for subclasses too, unlike a hardcoded Temperature(...)
        return cls((f - 32) * 5 / 9)

    @staticmethod
    def describe():
        # no implicit first argument at all -- just a plain function namespaced under the class
        return "Temperature is stored in Celsius internally"

class Kelvin(Temperature):
    @classmethod
    def from_fahrenheit(cls, f):
        base = super().from_fahrenheit(f)
        base.celsius += 273.15
        return base

t = Temperature.from_fahrenheit(98.6)
print(f"{t.celsius:.1f}C")
print(Temperature.describe())

k = Kelvin.from_fahrenheit(98.6)   # `cls` inside from_fahrenheit is Kelvin here, not Temperature
print(type(k).__name__, f"{k.celsius:.1f}")

Interview angle

A strong senior-level question is "implement your own version of @property using __get__/__set__" — it's a clean way to check whether a candidate understands Python's attribute-lookup machinery, or has only ever used property as a black box. The classmethod-as-alternative-constructor pattern (cls(...) inside a @classmethod) is also a great "do you write inheritance-safe code" check: a hardcoded class name instead of cls silently breaks for subclasses.

In the industry

Alternative constructors via @classmethod are everywhere in real APIs — dict.fromkeys, datetime.fromisoformat, and most ORMs' Model.from_json(...)-style factories all use this pattern specifically so subclasses inherit working constructors for free. Hand-written descriptors are rare in application code but foundational to libraries like Django (model fields), SQLAlchemy (columns), and attrs/pydantic — understanding the protocol is what lets you actually read how those libraries work instead of treating them as magic.