Descriptors
Descriptors
Section titled โDescriptorsโWhat it is
Section titled โWhat it isโ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.
Before this feature
Section titled โBefore this featureโ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 = valueAfter this feature
Section titled โAfter this featureโ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__ -> 25t.celsius = -300 # triggers Celsius.__set__ -> ValueErrorWhy this is better
Section titled โWhy this is betterโ- Reusable across attributes and classes: write the validation/computation logic once in a descriptor class, then attach it to as many attributes as needed โ
@propertycanโt be shared this way, since each@propertyis 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 = valuecan transparently validate, convert types, and stage a database write.
Key notes / edge cases
Section titled โKey notes / edge casesโ@propertyis 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 theCelsiusexample) 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
instanceparameter 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 accessinginstance.methodtriggers the functionโs__get__, which returns a bound method withselfalready filled in.
Quick practice
Section titled โQuick practiceโ-
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__`. -
Is
@propertyunrelated to descriptors, or built on them?Answer
Built on them โ `property` is itself a descriptor class implementing `__get__`/`__set__`/`__delete__`; `@property` is just a convenient way to create one from a method. -
Where does a descriptor instance live โ on the class or on each instance?
Answer
On 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.