Skip to content

Descriptors and Properties

Descriptors are one of the most important mechanisms in Python's object model. Bound methods, `property`, ORM fields, and many framework abstractions all depend on descriptor behavior.

Quick takeaway: a descriptor is an object that hooks attribute access. Data descriptors outrank instance dictionaries, while non-data descriptors do not. That single ordering rule explains a surprising amount of framework behavior.

Attribute Lookup Order First

Attribute lookup is not a simple dict lookup. Descriptor priority changes the result.

property Is Already a Descriptor

py
class Celsius:
    def __init__(self, value: float) -> None:
        self._value = value

    @property
    def value(self) -> float:
        return self._value

    @value.setter
    def value(self, number: float) -> None:
        if number < -273.15:
            raise ValueError("below absolute zero")
        self._value = number


temp = Celsius(20.0)
print(temp.value)
temp.value = 25.0

`property` is not mere syntax sugar. It creates a descriptor object stored on the class, and that object controls attribute access.

A Small Custom Descriptor

py
class PositiveNumber:
    def __set_name__(self, owner: type, name: str) -> None:
        self.private_name = f"_{name}"

    def __get__(self, instance: object | None, owner: type | None = None) -> object:
        if instance is None:
            return self
        return getattr(instance, self.private_name)

    def __set__(self, instance: object, value: int) -> None:
        if value <= 0:
            raise ValueError("positive only")
        setattr(instance, self.private_name, value)


class Product:
    stock = PositiveNumber()

    def __init__(self, stock: int) -> None:
        self.stock = stock


product = Product(10)
print(product.stock)

Framework field declarations often work like this: a descriptor instance is stored on the class and then participates in reads and writes through `__get__`, `__set__`, and `__set_name__`.

Data vs Non-Data Descriptors

  • data descriptor: defines __set__ or __delete__
  • non-data descriptor: usually defines only __get__
  • data descriptor beats the instance dictionary
  • non-data descriptor yields to the instance dictionary

Practical Connections

  • SQLAlchemy instrumented attributes
  • Pydantic field behavior and metadata access
  • custom computed or validated attributes

Official Sources

Built with VitePress for a Python 3.14 handbook.