Python Descriptor Protocol Explained Simply
Learn how Python's descriptor protocol powers properties, methods, and reusable attribute behavior. Understand `__get__`, `__set__`, and `__delete__` with clear examples.
Python’s Descriptor Protocol Explained – The Magic Behind Properties and Methods
If you’ve been using Python for a while, you’ve probably taken classes, properties, and methods for granted. But have you ever wondered what makes @property work? Or why a method automatically receives self when you call it on an instance? The secret lies in Python’s descriptor protocol. It’s one of those things that feels magical until you understand it. And once you do, it unlocks a whole new level of control over how objects behave.
Let’s break it down without the jargon—just clear, practical examples.
What Exactly is a Descriptor?
Simply put, a descriptor is an object that defines how attribute access works on another object. An attribute can be looked up (__get__), set (__set__), or deleted (__delete__). When you define any of these three methods in a class, you turn that class into a descriptor.
Here’s the simplest descriptor you’ll ever see:
class MyDescriptor:
def __get__(self, obj, objtype=None):
return "You just read this"
If you place an instance of MyDescriptor as a class attribute, accessing that attribute on an instance will call our __get__ method.
The Three Methods That Make It Work
| Method | Purpose | Example |
|---|---|---|
__get__(self, obj, objtype) |
Called when you read the attribute | instance.attr |
__set__(self, obj, value) |
Called when you assign to it | instance.attr = 5 |
__delete__(self, obj) |
Called when you delete it | del instance.attr |
Not all three are required. A descriptor that only implements __get__ is called a non-data descriptor. If it implements __set__ or __delete__, it becomes a data descriptor. This distinction matters because data descriptors take priority over instance attributes in lookup order.
A Real Example: Validated Attributes
Imagine you want a class where a certain attribute must always be a positive integer. Without descriptors, you’d pollute your code with validation logic everywhere. With descriptors, it’s clean.
class PositiveInt:
def __set_name__(self, owner, name):
self.name = name
def __get__(self, obj, objtype=None):
return obj.__dict__.get(self.name, 0)
def __set__(self, obj, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f"{self.name} must be a positive integer")
obj.__dict__[self.name] = value
Now you can use it in any class:
class Product:
price = PositiveInt()
p = Product()
p.price = 10 # Works
p.price = -5 # Raises ValueError
This pattern is how frameworks like Django or SQLAlchemy do field validation under the hood. PythonSkillset users often build similar validators for configuration classes.
Why Properties and Methods Are Descriptors
The @property decorator is actually a convenience wrapper around the descriptor protocol. When you write:
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14 * self._radius ** 2
Python turns area into a descriptor with only a __get__ method. That’s why circle.area computes on the fly.
Methods are also descriptors. When you call instance.method(), Python actually does:
- Look up
methodon the class – finds a function object. - That function’s
__get__method is called, binding the instance to the method. - The result is a bound method that already has
selffilled in.
How Lookup Order Works
When you access an attribute on an instance, Python follows this order:
- Data descriptors on the class hierarchy (those with
__set__or__delete__). - Instance attributes (stored in
obj.__dict__). - Non-data descriptors and other class attributes.
This is why you can override a property by assigning to the instance directly, but not a data descriptor. It’s a subtle but important detail when designing APIs.
When Should You Use Descriptors?
Descriptors shine when you need reusable attribute behavior. Common use cases include:
- Validation (type checking, range checks)
- Lazy loading (compute expensive values only when accessed)
- Logging access (audit trails for sensitive data)
- Implementing ORM fields
At PythonSkillset, we’ve seen descriptors used to build mini-frameworks for configuration systems, where each setting has its own validation, default, and change tracking.
A Word of Caution
Don’t reach for descriptors too quickly. They add complexity, and often a simple @property or even a regular method is enough. But when you find yourself repeating the same attribute logic across many classes, that’s your cue. Descriptors let you write that logic once and reuse it cleanly.
Next Steps
Play with the code. Create a descriptor that logs every read or write to a file. Then try building one that caches expensive computations. Descriptors are one of those Python features that feel like a superpower once you understand them. And the best part? You’ve been using them all along without knowing.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.