What are descriptors, and how does `@property` relate to them?
Quick Answer
A **descriptor** is any object whose class defines `__get__`, `__set__`, and/or `__delete__`, and is stored as a **class attribute** — when accessed through an instance, Python calls the descriptor's methods instead of returning it directly. `property` is the most common built-in descriptor; you can write your own **reusable** descriptors to share validation/behavior across many attributes or classes, which a single `@property` per attribute can't do.
Detailed Answer
The descriptor protocol
A class is a descriptor if it implements any of:
class Descriptor:
def __get__(self, obj, objtype=None): ...
def __set__(self, obj, value): ...
def __delete__(self, obj): ...
When such an object is assigned as a class attribute of another class, accessing that attribute through an instance routes through the descriptor's methods instead of Python's normal attribute lookup.
A reusable validating descriptor
class PositiveNumber:
def __set_name__(self, owner, name):
self._name = "_" + name
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self._name)
def __set__(self, obj, value):
if value <= 0:
raise ValueError(f"{self._name[1:]} must be positive")
setattr(obj, self._name, value)
class Circle:
radius = PositiveNumber() # reusable across classes/attributes
class Rectangle:
width = PositiveNumber()
height = PositiveNumber()
Unlike @property, which you write once per attribute, PositiveNumber
is written once and reused for radius, width, and height — this
is exactly how libraries like Django models and attrs/dataclasses
validators implement field-level behavior under the hood.
property is a descriptor
class property:
def __get__(self, obj, objtype=None): ...
def __set__(self, obj, value): ...
@property is a convenience wrapper that constructs exactly this kind of
descriptor object from the getter/setter/deleter functions you decorate —
it's the special case of "one descriptor instance per attribute, defined
inline," while a hand-written descriptor class is the general case of
"one reusable descriptor type, instantiated per attribute."
Data descriptors vs non-data descriptors
- Data descriptor: defines
__set__(and/or__delete__) — takes priority over instance__dict__even if the instance has its own entry with the same name. - Non-data descriptor: defines only
__get__— instance__dict__takes priority over it. This is how plain functions work as methods: a function is a non-data descriptor whose__get__produces a bound method, but you could shadow it by assigning directly intoinstance.__dict__["method_name"].
Interview-ready summary: Descriptors are the general mechanism behind
attribute access customization — property, bound methods, and
classmethod/staticmethod are all built on the descriptor protocol.
Write a custom descriptor when you need the same validation/behavior
reused across multiple attributes or classes; use @property for a
one-off attribute on a single class.