Descriptors: Reusable Attribute Control in Python
Learn how to implement reusable attribute control in Python using properties, descriptors, and decorators to manage class attributes with validation, type
Descriptors and Controlled Attributes in Python
Unlike properties, which offer a simple decorator-based approach to attribute access, descriptors provide a lower-level protocol for controlling how attributes behave across multiple classes. When properties fall short for reusable attribute logic, descriptors step in as Python’s mechanism for defining custom attribute behavior.
The Problem It Solves
Python developers often need to enforce validation rules or transform data when accessing object attributes. While properties work well for individual attributes within a single class, they become repetitive when the same logic applies to multiple attributes or across different classes. Descriptors solve this by allowing developers to define attribute behavior once and reuse it wherever needed.
Descriptors control what happens when an attribute is accessed, set, or deleted. They implement a protocol through special methods: __get__, __set__, and __delete__. When an object’s attribute is a descriptor, Python automatically calls these methods instead of performing default attribute access. This enables centralized control over attribute behavior without duplicating code.
The descriptor protocol powers many Python features developers use daily. Properties themselves are implemented using descriptors, as are class methods and static methods. Understanding descriptors reveals how these built-in features work under the hood.
How It Works
A descriptor is any object that defines at least one of three methods: __get__(self, obj, type=None), __set__(self, obj, value), or __delete__(self, obj). When a descriptor is assigned as a class attribute, Python intercepts attribute access on instances of that class.
For example, a descriptor that validates numeric ranges might look like this:
def __init__(self, min_value, max_value):
self.min_value = min_value
self.max_value = max_value
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, type=None):
if obj is None:
return self
return obj.__dict__.get(self.name)
def __set__(self, obj, value):
if not (self.min_value <= value <= self.max_value):
raise ValueError(f"{self.name} must be between {self.min_value} and {self.max_value}")
obj.__dict__[self.name] = value
The __set_name__ method, added in Python 3.6, automatically receives the attribute name when the descriptor is assigned to a class. This eliminates the need to manually specify names.
Descriptors fall into two categories: data descriptors and non-data descriptors. Data descriptors define both __get__ and __set__, while non-data descriptors define only __get__. This distinction matters because data descriptors take precedence over instance dictionary entries during attribute lookup, while non-data descriptors do not.
Setup Guide
To create a reusable descriptor, define a class with the appropriate protocol methods. The descriptor is then assigned as a class attribute:
price = RangeValidator(0, 10000)
quantity = RangeValidator(0, 1000)
def __init__(self, price, quantity):
self.price = price
self.quantity = quantity
When creating an instance, the descriptor’s __set__ method validates values automatically. Attempting to assign an out-of-range value raises a ValueError.
For descriptors that need to store data per instance, use the instance’s __dict__ rather than storing data on the descriptor itself. Since the descriptor is a class attribute shared across all instances, storing data directly on the descriptor would cause instances to overwrite each other’s values.
Ecosystem
Descriptors integrate with Python’s attribute lookup mechanism, which follows a specific order. When accessing an attribute, Python first checks for data descriptors in the class hierarchy, then the instance dictionary, then non-data descriptors, and finally raises AttributeError if nothing is found.
This protocol enables advanced patterns like lazy evaluation, type checking, and automatic database field mapping in ORMs. Many popular frameworks rely on descriptors internally. Django’s model fields use descriptors to map Python attributes to database columns, while SQLAlchemy employs them for similar purposes.
For simpler cases where attribute logic applies to a single class, properties remain the more readable choice. Descriptors shine when building reusable components or frameworks that need consistent attribute behavior across multiple classes.
Source: machinelearningmastery.com
Related Tips
NES Emulator Runs Inside AI Model Weights
Researchers demonstrate a novel technique where a Nintendo Entertainment System emulator is encoded directly into neural network weights, allowing the AI model
Fine-Tuning vs Frozen LLMs for Classification
This article examines the trade-offs between fine-tuning large language models and using frozen models for classification tasks, comparing performance, cost,
AI "Vibe Coding" Transforms Game Development in 2026
Game developers in 2026 are using AI vibe coding tools that transform natural language descriptions and creative prompts into functional game code,