
Mastering Python's Descriptor Protocol for Flexible Object Attributes
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
passDescriptors 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] = valueNow, 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 = ageThis 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:
| Feature | Descriptors | @property Decorator |
|---|---|---|
| Reusability | High (can be reused across classes) | Low (tied to a specific class) |
| Multiple attributes | Yes (one class for multiple fields) | No (each property is separate) |
| Performance | Slightly slower (more indirection) | Slightly faster (optimized in C) |
| Shared logic | Yes (centralized logic) | No (repeated code) |
| Use in frameworks | Common (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 aNow, 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
- Use
__set_name__for dynamic attribute names — This allows descriptors to adapt to the name they are assigned under. - Avoid overusing descriptors — For simple validation or computed properties,
@propertymay be sufficient. - Keep descriptors focused — A descriptor should encapsulate a single concern, such as validation or lazy evaluation.
- Test thoroughly — Descriptors can introduce subtle bugs, especially when used with inheritance or metaclasses.
- Document clearly — Since descriptors are not as visible as regular methods, good documentation is essential.
Learn more with useful resources
- Python Descriptor HowTo Guide – Official documentation from Python.org.
- Real Python: Understanding Descriptors – A comprehensive guide with examples.
- Python Cookbook, 3rd Edition by David Beazley and Brian K. Jones – Advanced Python patterns including descriptors.
