A descriptor in Python is an object that defines how attribute access is managed. Descriptors allow you to customize how attributes are retrieved, set, and deleted. They are a powerful feature of Python’s object model and are used behind the scenes in many Python mechanisms like methods, properties, and class variables.
Descriptors are defined by implementing any of the following special methods in a class:
- __get__ (for retrieving an attribute)
- __set__ (for setting an attribute)
- __delete__ (for deleting an attribute).
How Descriptors Work?
A descriptor is a class that implements one or more of the descriptor methods (__get__, __set__, and __delete__), and is used in another class as an attribute. Python invokes these methods when you interact with the attribute.
Descriptor Protocol
__get__(self, instance, owner):
- Controls attribute access (when you retrieve the attribute).
- instance: the instance that the attribute is being accessed on (or None if accessed through the class).
- owner: the owner class (usually the class where the descriptor is defined).
__set__(self, instance, value):
- Controls attribute assignment.
- instance: the instance that the attribute is being set on.
- value: the value being assigned.
__delete__(self, instance):
- Controls attribute deletion.
- instance: the instance that the attribute is being deleted from.
Basic Descriptor Example
class Descriptor:
def __get__(self, instance, owner):
return f"Getting attribute from {instance}"
def __set__(self, instance, value):
print(f"Setting attribute to {value}")
instance._value = value
def __delete__(self, instance):
print("Deleting attribute")
del instance._value
class MyClass:
attr = Descriptor() # Using Descriptor as an attribute
obj = MyClass()
obj.attr = 42 # Setting attribute to 42
print(obj.attr) # Getting attribute from <__main__.MyClass object at 0x...>
del obj.attr # Deleting attribute
In this example, Descriptor defines the behavior of attr in MyClass. Accessing, setting, and deletingobj.attr triggers the descriptor’s methods (__get__, __set__, and __delete__).
Types of Descriptors
Data Descriptor:
- Implements both __get__ and __set__ (or __delete__).
- Controls both attribute access and assignment.
Example:
class DataDescriptor:
def __get__(self, instance, owner):
return "Data descriptor: accessed"
def __set__(self, instance, value):
print("Data descriptor: value set")
class MyClass:
attr = DataDescriptor()
obj = MyClass()
obj.attr = 10 # Output: Data descriptor: value set
print(obj.attr) # Output: Data descriptor: accessed
Non-data Descriptor:
- Only implements __get__.
- Controls only attribute access, not assignment. When you assign directly to the attribute, Python bypasses the __get__ method.
Example:
class NonDataDescriptor:
def __get__(self, instance, owner):
return "Non-data descriptor: accessed"
class MyClass:
attr = NonDataDescriptor()
obj = MyClass()
print(obj.attr) # Output: Non-data descriptor: accessed
obj.attr = 20 # This overrides the descriptor
print(obj.attr) # Output: 20 (no longer calls __get__)
Practical Use of Descriptors
Descriptors are useful when you need to manage how certain attributes of your class behave. Common use cases include:
- Properties (via property()): The property() function in Python is a descriptor that wraps getter, setter, and deleter methods.
class MyClass:
def __init__(self, value):
self._value = value
def get_value(self):
return self._value
def set_value(self, value):
self._value = value
value = property(get_value, set_value)
obj = MyClass(10)
print(obj.value) # Uses get_value
obj.value = 20 # Uses set_value
- Static and Class Methods: Both static methods (staticmethod) and class methods (classmethod) are implemented using descriptors.
class MyClass:
@staticmethod
def static_method():
print("Static method")
MyClass.static_method() # Calls the static method descriptor
- Managing Computed Attributes: Descriptors are helpful when you need to control how attributes are computed or validated.
class Celsius:
def __init__(self, temperature=0):
self._temperature = temperature
def __get__(self, instance, owner):
return (self._temperature - 32) * 5/9 # Convert to Celsius
def __set__(self, instance, value):
self._temperature = value * 9/5 + 32 # Store Fahrenheit
class Weather:
temperature = Celsius()
w = Weather()
w.temperature = 100 # Set temperature in Celsius
print(w.temperature) # Output: 100°C -> 37.77777777777778°C
Advanced Descriptor Example: Validating Descriptors
You can use descriptors to enforce validation logic when setting an attribute.
class PositiveValue:
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 MyClass:
attr = PositiveValue()
obj = MyClass()
obj.attr = 10 # Works fine
print(obj.attr) # Outputs: 10
obj.attr = -5 # Raises ValueError: Value must be positive
Common Mistakes with Descriptors
- Forgetting to Use instance._value for Attribute Storage: Descriptors usually store values in the instance (instance._value), not in the descriptor object itself. Otherwise, all instances will share the same value.
Wrong:
class Descriptor:
def __set__(self, instance, value):
self.value = value # Stored in descriptor, not instance!
obj1 = MyClass()
obj2 = MyClass()
obj1.attr = 10
print(obj2.attr) # Outputs 10, even though we expect it to be separate
Correct:
class Descriptor:
def __set__(self, instance, value):
instance._value = value # Stored in the instance!
obj1 = MyClass()
obj2 = MyClass()
obj1.attr = 10
print(obj2.attr) # Now it's independent!
- Non-data Descriptor Being Overwritten: A non-data descriptor can be overwritten by direct assignment because it doesn’t implement __set__. This can lead to unintended behavior.