Python - Python Descriptors Protocol (Detailed Explanation)

The descriptor protocol is a fundamental part of Python’s object model that allows you to customize how attributes are accessed, assigned, and deleted. It is a low-level mechanism that powers many built-in features such as methods, properties, static methods, and class methods.

At its core, a descriptor is any object that implements one or more of the following special methods:

  • __get__(self, instance, owner)

  • __set__(self, instance, value)

  • __delete__(self, instance)

These methods control attribute access when the descriptor is used as a class attribute.


1. How Descriptors Work

When you define an attribute in a class and assign it an object that implements descriptor methods, Python automatically invokes those methods during attribute access.

For example:

class MyDescriptor:
    def __get__(self, instance, owner):
        print("Getting value")
        return 42

class MyClass:
    attr = MyDescriptor()

obj = MyClass()
print(obj.attr)

Instead of directly returning attr, Python calls MyDescriptor.__get__(). This is what makes descriptors powerful—they intercept attribute access.


2. Types of Descriptors

Descriptors are categorized based on which methods they implement:

Data Descriptors

If a descriptor defines __set__ or __delete__ (or both), it is called a data descriptor.

Example:

class DataDescriptor:
    def __get__(self, instance, owner):
        return instance._value

    def __set__(self, instance, value):
        instance._value = value

Data descriptors have higher priority than instance attributes during lookup.


Non-Data Descriptors

If a descriptor only defines __get__, it is called a non-data descriptor.

Example:

class NonDataDescriptor:
    def __get__(self, instance, owner):
        return "Hello"

Non-data descriptors can be overridden by instance attributes.


3. Attribute Lookup Order

When accessing obj.attr, Python follows this order:

  1. Check if attr is a data descriptor in the class.

  2. Check if attr exists in the instance dictionary (obj.__dict__).

  3. Check if attr is a non-data descriptor in the class.

  4. Check class attributes and parent classes.

This order explains why data descriptors override instance variables but non-data descriptors do not.


4. Practical Example: Controlled Attribute Access

Descriptors are often used to enforce rules on attributes.

class PositiveNumber:
    def __get__(self, instance, owner):
        return instance._value

    def __set__(self, instance, value):
        if value < 0:
            raise ValueError("Value must be positive")
        instance._value = value

class Product:
    price = PositiveNumber()

p = Product()
p.price = 100   # Valid
# p.price = -50  # Raises error

Here, the descriptor ensures that only positive values are assigned.


5. Relationship with Built-in Features

Many Python features are implemented using descriptors:

  • property() uses descriptor methods to manage getters and setters.

  • Functions defined in a class become bound methods via descriptors.

  • staticmethod and classmethod rely on descriptor behavior.

Example using property:

class Example:
    def __init__(self):
        self._x = 0

    def get_x(self):
        return self._x

    def set_x(self, value):
        self._x = value

    x = property(get_x, set_x)

The property object internally implements descriptor methods.


6. Advanced Use Case: Reusable Logic

Descriptors allow reusable attribute behavior across multiple classes.

class Typed:
    def __init__(self, expected_type):
        self.expected_type = expected_type

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError("Invalid type")
        instance.__dict__[self.name] = value

    def __set_name__(self, owner, name):
        self.name = name

class Person:
    age = Typed(int)
    name = Typed(str)

p = Person()
p.age = 25
p.name = "John"

This approach avoids repeating validation logic for each attribute.


7. Key Advantages

  • Fine-grained control over attribute access

  • Code reuse across classes

  • Foundation for advanced frameworks and ORMs

  • Enables clean separation of logic and data


Conclusion

The descriptor protocol is a powerful and often underappreciated feature in Python. It operates behind the scenes in many core language features and enables developers to build highly flexible and reusable components. By understanding descriptors, you gain deeper insight into Python’s object model and unlock the ability to design more sophisticated abstractions.