What is a Descriptor?

A descriptor is an object that defines any of the following methods: __get__, __set__, or __delete__. When a class attribute is a descriptor, Python automatically uses these methods to manage access to the attribute, overriding the default behavior.

Here is the minimal structure of a descriptor:

class Descriptor:
    def __get__(self, instance, owner_class):
        # Called to get the attribute value
        pass

    def __set__(self, instance, value):
        # Called to set the attribute value
        pass

    def __delete__(self, instance):
        # Called to delete the attribute
        pass

Descriptors are used by Python internally in many features, such as property, classmethod, and staticmethod.


Real-World Example: Validating User Data with a Descriptor

A common use of descriptors is to enforce data validation across multiple instances. Let's create a descriptor that ensures a user's age is an integer and is within a valid range.

class AgeValidator:
    def __init__(self, min_age=0, max_age=120):
        self.min_age = min_age
        self.max_age = max_age

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

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, int):
            raise ValueError(f"{self.name} must be an integer")
        if not (self.min_age <= value <= self.max_age):
            raise ValueError(f"{self.name} must be between {self.min_age} and {self.max_age}")
        instance.__dict__[self.name] = value

Now, use this descriptor in a User class:

class User:
    age = AgeValidator(min_age=18, max_age=99)

    def __init__(self, name, age):
        self.name = name
        self.age = age

This ensures that all User instances have a valid age within the specified range.


Comparing Descriptors with Properties

Both descriptors and properties provide a way to customize attribute access. However, they differ in flexibility and use cases. The table below compares the two:

FeatureDescriptors@property Decorator
ReusabilityHigh (can be reused across classes)Low (tied to a specific class)
Multiple attributesYes (one class for multiple fields)No (each property is separate)
PerformanceSlightly slower (more indirection)Slightly faster (optimized in C)
Shared logicYes (centralized logic)No (repeated code)
Use in frameworksCommon (e.g., Django, SQLAlchemy)Less common

While @property is easier to write for simple cases, descriptors are better for complex or reusable logic.


Advanced Use Case: Lazy Evaluation with Descriptors

A common advanced use of descriptors is to implement lazy evaluation, where a value is computed only when needed and cached for subsequent access.

Here's an example using a descriptor to compute and cache a Fibonacci number:

class LazyFibonacci:
    def __init__(self, index):
        self.index = index

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

    def __get__(self, instance, owner):
        if not hasattr(instance, self.name):
            result = self.compute_fibonacci(self.index)
            instance.__dict__[self.name] = result
        return instance.__dict__[self.name]

    def compute_fibonacci(self, n):
        a, b = 0, 1
        for _ in range(n):
            a, b = b, a + b
        return a

Now, use this in a class:

class FibonacciCache:
    fib_10 = LazyFibonacci(10)
    fib_20 = LazyFibonacci(20)

Each attribute is computed only once, and the result is cached in the instance.


Best Practices for Using Descriptors

  1. Use __set_name__ for dynamic attribute names — This allows descriptors to adapt to the name they are assigned under.
  2. Avoid overusing descriptors — For simple validation or computed properties, @property may be sufficient.
  3. Keep descriptors focused — A descriptor should encapsulate a single concern, such as validation or lazy evaluation.
  4. Test thoroughly — Descriptors can introduce subtle bugs, especially when used with inheritance or metaclasses.
  5. Document clearly — Since descriptors are not as visible as regular methods, good documentation is essential.

Learn more with useful resources