Introduction

Python’s descriptor protocol is a powerful yet often overlooked feature that allows fine-grained control over attribute access in objects. It forms the foundation of built-in mechanisms like @property, method binding, and data validation. In this article, we will dive deep into descriptors, explore their use cases, and understand how to leverage them effectively.


What is a Descriptor in Python?

A descriptor is a class that implements any of the following methods:

  • __get__(self, instance, owner) – Retrieves the attribute value
  • __set__(self, instance, value) – Sets the attribute value
  • __delete__(self, instance) – Deletes the attribute

Descriptors allow custom attribute access, providing more control over object behavior.


Types of Descriptors

Descriptors fall into two main categories:

  1. Data Descriptors – Implement both __get__ and __set__.
  2. Non-Data Descriptors – Implement only __get__, making them read-only.

Implementing a Simple Descriptor

Let’s start with a basic descriptor that enforces type validation.

class TypedAttribute:  
def __init__(self, attr_type):  
self.attr_type = attr_type  
self.data = {}

    def __get__(self, instance, owner):  
        return self.data.get(instance)  

    def __set__(self, instance, value):  
        if not isinstance(value, self.attr_type):  
            raise TypeError(f"Expected {self.attr_type}, got {type(value)}")  
        self.data[instance] = value  

# Usage
class Person:  
age = TypedAttribute(int)

person = Person()  
person.age = 30  
print(person.age)  # 30
# person.age = "thirty"  # Raises TypeError

Using Descriptors for Property Management

Descriptors can replace the @property decorator while offering more flexibility.

class PropertyDescriptor:  
def __init__(self, default=None):  
self.value = default

    def __get__(self, instance, owner):  
        return self.value  

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

class Car:  
speed = PropertyDescriptor(0)

car = Car()  
car.speed = 100  
print(car.speed)  # 100  

Unlike @property, this approach allows reusability across multiple attributes.


Leveraging Descriptors for Logging and Access Control

Descriptors can also be used to implement logging, caching, and access control.

class LoggedAttribute:  
def __init__(self, name):  
self.name = name

    def __get__(self, instance, owner):  
        value = instance.__dict__.get(self.name)  
        print(f"Accessing {self.name}: {value}")  
        return value  

    def __set__(self, instance, value):  
        print(f"Setting {self.name} to {value}")  
        instance.__dict__[self.name] = value  

class User:  
name = LoggedAttribute("name")

user = User()  
user.name = "Alice"  # Setting name to Alice  
print(user.name)  # Accessing name: Alice  

This descriptor automatically logs every access and modification.


Data vs. Non-Data Descriptors

Non-data descriptors do not define __set__, making them read-only unless overridden in __dict__.

class ReadOnlyDescriptor:  
def __get__(self, instance, owner):  
return "Read-Only Value"

class Example:  
value = ReadOnlyDescriptor()

e = Example()  
print(e.value)  # Read-Only Value  
e.value = "New Value"  # Overrides in __dict__, NOT descriptor  

Non-data descriptors allow local attribute overrides, whereas data descriptors take precedence.


The @property Decorator vs. Descriptors

Python’s built-in @property is implemented using descriptors internally.

class Example:  
@property  
def value(self):  
return "Property Method"

e = Example()  
print(e.value)  # Property Method  
print(e.__class__.value)  # Shows a property object (descriptor)  

Using descriptors directly provides greater control, allowing shared logic across multiple classes.


When to Use Descriptors

Fine-Grained Attribute Control – Validation, access logging, computed properties
Reusable Property Management – Avoid repetitive use of @property
Performance Optimization – Reduce redundant computations via caching
Encapsulation & Abstraction – Cleanly separate attribute logic


Conclusion

Python’s descriptor protocol is a powerful tool for managing object attributes. By mastering descriptors, you can build flexible, reusable, and efficient property management systems.

Want to explore advanced OOP techniques in Python? Stay tuned for our next deep dive into Python’s Metaclasses and Dynamic Class Creation!