Skip to the content.

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


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

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.