Skip to content

Descriptors

A descriptor is any object whose class defines __get__, __set__, or __delete__, and which is stored as a class attribute. When you access instance.attr, if attr on the class is a descriptor, Python calls its __get__ (or __set__/__delete__) instead of just returning the stored value directly. This is the mechanism underneath @property, and even underneath how instance methods themselves work.

Without descriptors, adding validation or computed behavior to attribute access means writing explicit getter/setter methods (get_x()/set_x()), which breaks the plain-attribute syntax callers expect:

class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def set_celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._celsius = value
class Celsius:
def __get__(self, instance, owner):
return instance._celsius
def __set__(self, instance, value):
if value < -273.15:
raise ValueError("Below absolute zero")
instance._celsius = value
class Temperature:
celsius = Celsius() # a descriptor, shared across all Temperature instances
def __init__(self, celsius):
self.celsius = celsius # triggers Celsius.__set__
t = Temperature(25)
print(t.celsius) # triggers Celsius.__get__ -> 25
t.celsius = -300 # triggers Celsius.__set__ -> ValueError
  • Reusable across attributes and classes: write the validation/computation logic once in a descriptor class, then attach it to as many attributes as needed โ€” @property canโ€™t be shared this way, since each @property is tied to one specific method.
  • Explains โ€œhow Python actually worksโ€: understanding descriptors demystifies why @property, @staticmethod, @classmethod, and even plain instance methods behave the way they do โ€” theyโ€™re all implemented as descriptors under the hood.
  • Framework building blocks: ORMs (Django model fields, SQLAlchemy columns) use descriptors so model.field = value can transparently validate, convert types, and stage a database write.
  • @property is really just a convenient, built-in descriptor โ€” @property def x(self): ... is syntactic sugar for a one-off descriptor class wired up automatically.
  • Data descriptors (define __set__ or __delete__, like the Celsius example) take priority over instance __dict__; non-data descriptors (only __get__, like plain functions/methods) can be overridden by an instance attribute of the same name.
  • The descriptor instance lives on the class, not the instance โ€” this is exactly why it needs the instance parameter in __get__/__set__, to know which specific objectโ€™s data to read or write.
  • Descriptors are the reason instance methods โ€œknowโ€ self: a function is a non-data descriptor, and accessing instance.method triggers the functionโ€™s __get__, which returns a bound method with self already filled in.
  1. What two methods does an objectโ€™s class need for it to act as a โ€œdata descriptorโ€?

    Answer`__set__` (and/or `__delete__`), in addition to `__get__` โ€” having `__set__`/`__delete__` is what makes it a *data* descriptor, which takes priority over instance `__dict__`.
  2. Is @property unrelated to descriptors, or built on them?

    AnswerBuilt on them โ€” `property` is itself a descriptor class implementing `__get__`/`__set__`/`__delete__`; `@property` is just a convenient way to create one from a method.
  3. Where does a descriptor instance live โ€” on the class or on each instance?

    AnswerOn the class, shared by all instances โ€” which is why `__get__`/`__set__` receive the specific `instance` as an argument, to know which object's data to operate on.