Author: Amin Boulouma, Software Engineer Github source code: https://github.com/aminblm/ai_systems_design_from_scratch Engineering Blog: https://aminblm.github.io/ai_systems_design_from_scratch/blog/
The Ultimate Guide to Mastering Python Descriptors (Without Common Bugs)
Most developers use property, classmethod, and staticmethod every day without realizing they all share a common mechanism: Descriptors. Descriptors are the underlying protocol that allows you to customize attribute access. If you are building frameworks that require strict data validation, lazy loading, or dynamic attribute calculation, descriptors are the most powerful tool in your arsenal.
The Core Concept
A Descriptor is any object that defines at least one of the following methods: __get__, __set__, or __delete__. When you access an attribute on an instance, Python checks if the attribute is a descriptor; if it is, the descriptor overrides the default attribute access logic.
Glossary for Beginners
- Descriptor Protocol: The set of dunder methods (
__get__,__set__,__delete__) that define a descriptor. - Managed Attribute: An attribute whose value is controlled by an external object (the descriptor) rather than being stored directly in the instance
__dict__. - Owner Class: The class in which the descriptor instance is assigned as a class attribute.
- Instance Access: Accessing a descriptor via an instance (
obj.descriptor) versus the class (MyClass.descriptor).
Why We Choose Descriptors Over @property
We choose descriptors over @property when we have repetitive attribute logic. If you find yourself writing the same validation logic (e.g., “this field must be a positive integer”) across multiple classes, a descriptor allows you to encapsulate that logic into a reusable class.
Why X over Y? We choose descriptors because they follow the DRY (Don’t Repeat Yourself) principle at the attribute level. @property is great for one-off logic, but descriptors are the architecture of choice for building custom validation libraries or ORMs (Object Relational Mappers).
Implementation: The Descriptor Pattern
Simple Example: Reusable Validation
class PositiveInt:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
return instance.__dict__.get(self.name)
def __set__(self, instance, value):
if value < 0:
raise ValueError(f"{self.name} must be positive")
instance.__dict__[self.name] = value
class Product:
price = PositiveInt()
quantity = PositiveInt()
# Usage
p = Product()
p.price = 10 # Works
# p.quantity = -1 # Raises ValueError
Complex Example: Production-Grade Lazy Loading
In data-heavy systems, we use descriptors to “lazy load” expensive resources, ensuring the object doesn’t consume memory until the attribute is actually accessed.
class LazyResource:
def __init__(self, factory):
self.factory = factory
self.name = None
def __set_name__(self, owner, name):
self.name = name
def __get__(self, instance, owner):
if instance is None: return self
value = self.factory()
setattr(instance, self.name, value)
return value
class DatabaseConnector:
# Expensive connection created only on first access
connection = LazyResource(lambda: "Connected to Production DB")
db = DatabaseConnector()
print(db.connection) # Triggered now
Quick Reference: Descriptor Methods
| Method | Trigger | Purpose |
|---|---|---|
__get__ |
Accessing attribute | Define custom retrieval logic |
__set__ |
Assigning attribute | Define validation/storage logic |
__delete__ |
Deleting attribute | Define custom deletion logic |
__set_name__ |
Class creation | Capture the attribute name |
Developer Checklist
- Is your descriptor logic performant? (Remember, it runs on every access).
- Have you used
__set_name__to automatically track the attribute name, avoiding hard-coded strings? - Are you storing state in
instance.__dict__or in the descriptor itself? (Storing in the descriptor makes it a “shared” attribute across all instances, which is usually a bug). - Is the descriptor complexity justified, or could a simple helper function suffice?
TL;DR Summary
Stop duplicating validation logic. Descriptors allow you to write reusable attribute-access rules that you can “plug and play” across your codebase. They are the backbone of professional-grade Python frameworks. Master them to build clean, declarative, and robust APIs.