What are descriptors, and how does `@property` relate to them?

7 minadvancedoopdescriptorsadvanced

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 into instance.__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.