Descriptors
Objects that define __get__, __set__, or __delete__ and control attribute access on classes that own them.
Descriptors = a bouncer at the door of an attribute. @property is a bouncer that intercepts every get/set. A non-data descriptor is a lazy bouncer who only checks on the way in (__get__), not out.
@property is a built-in descriptor. @staticmethod and @classmethod are too. A data descriptor defines both __get__ and __set__ (or __delete__) — it takes priority over instance __dict__. A non-data descriptor defines only __get__ — instance __dict__ takes priority. Descriptors only work when defined on a class, not on an instance.
Attribute lookup order: (1) data descriptors from the class hierarchy, (2) instance __dict__, (3) non-data descriptors and other class attributes. This is why @property (data descriptor) can intercept writes even if an instance dict entry exists. Descriptors are the mechanism behind functions becoming bound methods — function objects implement __get__ which returns a bound method when accessed via an instance. __slots__ uses descriptors internally. Writing a reusable validation descriptor (e.g., one that type-checks all instances of a class) is a common senior interview exercise.
Descriptors are the protocol behind @property, @classmethod, @staticmethod, and even how methods work. A descriptor defines __get__ and optionally __set__/__delete__ on a class to intercept attribute access. Data descriptors (with __set__) override instance __dict__. Non-data descriptors don't. This is why you can define a @property and instance.x = value still routes through the setter. I use custom descriptors for reusable validation logic across multiple classes.
Descriptors only activate when defined on the class, not on an instance. obj.x = some_descriptor_instance does NOT activate the descriptor protocol — it just sets a value in obj.__dict__.