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:
-
Check if
attris a data descriptor in the class. -
Check if
attrexists in the instance dictionary (obj.__dict__). -
Check if
attris a non-data descriptor in the class. -
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.
-
staticmethodandclassmethodrely 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.