Skip to content

Descriptors & Attribute Hooks

In Python, attribute access (obj.attr) is not a simple dictionary lookup. It is controlled by a powerful mechanism called the Descriptor Protocol. Descriptors are the hidden gears behind properties, methods, staticmethod, and classmethod.

Understanding descriptors allows you to create reusable, intelligent attributes that can validate data, perform lazy calculation, or log access automatically.


An object is a descriptor if it defines any of these three methods:

  1. __get__(self, instance, owner): Called when you read the attribute.
  2. __set__(self, instance, value): Called when you write to the attribute.
  3. __delete__(self, instance): Called when you del the attribute.
validator_descriptor.py
class NonNegative:
"""A descriptor that ensures a value is never below zero."""
def __init__(self, name):
self.name = name
def __get__(self, instance, owner):
# 'instance' is the object using the descriptor (e.g., the car)
# 'owner' is the class (e.g., Car)
return instance.__dict__.get(self.name, 0)
def __set__(self, instance, value):
if value < 0:
raise ValueError(f"{self.name} cannot be negative!")
instance.__dict__[self.name] = value
class Car:
# Descriptors MUST be defined at the class level
speed = NonNegative("speed")
c = Car()
c.speed = 100
# c.speed = -20 # Raises ValueError

This distinction is crucial for understanding how Python resolves names.

Define both __get__ and __set__. They take precedence over an object’s __dict__. Even if you try to manually set obj.attr = 10, Python will still call the descriptor’s __set__ method.

Define only __get__. They are used for methods. If an object has a variable in its __dict__ with the same name, the instance variable wins. This is how Python allows you to “overwrite” a method on a specific instance.


The @property decorator is actually just a convenient way to create a descriptor.

When you write:

@property
def x(self): return self._x

Python creates a property object (a data descriptor) that stores your function and calls it inside its own __get__ method.


When you run x = obj.attr, Python follows this high-priority search:

  1. Data Descriptor: Check if attr is a data descriptor in the class (or its parents).
  2. Instance Dictionary: Check if attr exists in obj.__dict__.
  3. Non-Data Descriptor: Check if attr is a non-data descriptor (like a method) in the class.
  4. Class Attribute: Check if attr is a simple variable in the class.
  5. __getattr__: If all else fails, call the __getattr__ hook if it exists.

MethodRoleCalled When…
__get__Retrievalvalue = obj.attr
__set__Assignmentobj.attr = value
__delete__Deletiondel obj.attr
__set_name__IdentificationCalled when the class is defined (Py 3.6+).