🧠 Python DeepCuts — 💡 The Descriptor Protocol
Posted on: November 26, 2025
Description:
If you’ve ever used @property, @classmethod, @staticmethod, ORMs like Django/SQLAlchemy, or dataclasses — you’ve used descriptors.
Descriptors are one of Python’s most powerful and elegant internals.
They give objects the ability to control attribute access at the class level.
Any object implementing one or more of the following:
- get(self, instance, owner)
- set(self, instance, value)
- delete(self, instance)
is a descriptor.
Let’s break down how they work and why they matter.
🧩 What Is a Descriptor?
A descriptor is simply a Python object with a get, set, or delete method.
This example logs when a value is accessed or updated:
class LoggedAttribute:
def __init__(self, initial):
self.value = initial
def __get__(self, instance, owner):
print("Accessing attribute...")
return self.value
def __set__(self, instance, value):
print("Setting attribute...")
self.value = value
class Demo:
x = LoggedAttribute(10)
obj = Demo()
print(obj.x) # __get__
obj.x = 20 # __set__
Descriptors intercept attribute lookups before Python returns a value.
🧠 @property Is Just a Descriptor
When you define a property, Python turns it into a descriptor behind the scenes.
class Temperature:
def __init__(self, celsius):
self._c = celsius
@property
def celsius(self):
return self._c
@celsius.setter
def celsius(self, value):
if value < -273.15:
raise ValueError("Below absolute zero")
self._c = value
- The getter acts like get
- The setter acts like set
@property is one of the most user-friendly wrappers around the descriptor protocol.
🔍 How Methods Work Using Descriptors
When you access a function through a class instance, Python turns it into a bound method.
This happens via descriptors.
class Example:
def greet(self):
print("Hello from instance!")
e = Example()
e.greet() # bound method
Functions implement get, allowing them to bind self automatically.
Without descriptors, self would NEVER be injected automatically.
🧱 staticmethod & classmethod Are Descriptors
Both use descriptors to change how functions receive arguments.
class Tools:
@staticmethod
def ping():
return "Static call"
@classmethod
def identify(cls):
return f"Called on {cls.__name__}"
| Decorator | What it passes |
|---|---|
| @staticmethod | passes nothing |
| @classmethod | passes cls |
Both work because they use descriptors internally.
🏗️ A Practical Example: ORM-Style Field
Frameworks like Django and SQLAlchemy use descriptors to define fields.
class Field:
def __init__(self):
self.private_name = None
def __set_name__(self, owner, name):
self.private_name = "_" + name
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name, None)
def __set__(self, instance, value):
setattr(instance, self.private_name, value)
Usage:
class User:
name = Field()
age = Field()
u = User()
u.name = "Abhijith"
u.age = 29
This is the core of how ORMs manage attributes.
🔧 set_name: Giving Descriptors Their Attribute Name
Python 3.6 introduced set_name, called automatically when the class is created.
class Label:
def __set_name__(self, owner, name):
print(f"Binding descriptor to: {name}")
It is used heavily inside ORM system fields, dataclasses, and attrs.
✅ Key Points
- Descriptors are the backbone of Python’s attribute system.
- @property, methods, classmethod, staticmethod → all built on descriptors.
- ORMs, dataclasses, and frameworks use descriptors for controlled attribute access.
- set_name helps descriptors know which attribute name they belong to.
- A descriptor is triggered whenever an attribute is:
- accessed (get)
- assigned (set)
- deleted (delete)
Understanding descriptors unlocks Python’s most advanced patterns.
Code Snippet:
import inspect
class LoggedAttribute:
def __init__(self, initial):
self.value = initial
def __get__(self, instance, owner):
print("Accessing attribute...")
return self.value
def __set__(self, instance, value):
print("Setting attribute...")
self.value = value
class Demo:
x = LoggedAttribute(10)
obj = Demo()
print(obj.x) # calls __get__
obj.x = 20 # calls __set__
print(obj.x)
class Temperature:
def __init__(self, celsius):
self._c = celsius
@property
def celsius(self): # behaves like __get__
return self._c
@celsius.setter
def celsius(self, value): # behaves like __set__
if value < -273.15:
raise ValueError("Below absolute zero")
self._c = value
t = Temperature(25)
print(t.celsius)
t.celsius = 30
print(t.celsius)
class Example:
def greet(self):
print("Hello from instance!")
# Unbound function
print(Example.greet)
# Bound method (self will be passed automatically)
e = Example()
print(e.greet)
# Call it
e.greet()
class Tools:
@staticmethod
def ping():
return "Static call"
@classmethod
def identify(cls):
return f"Called on {cls.__name__}"
print(Tools.ping()) # No instance or class passed
print(Tools.identify()) # cls passed automatically
class Field:
def __init__(self):
self.private_name = None
def __set_name__(self, owner, name):
# called once when class is created
self.private_name = "_" + name
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.private_name, None)
def __set__(self, instance, value):
setattr(instance, self.private_name, value)
class User:
name = Field()
age = Field()
u = User()
u.name = "Sam"
u.age = 29
print(u.name, u.age)
class Label:
def __set_name__(self, owner, name):
print(f"Binding descriptor to: {name}")
def __get__(self, instance, owner):
return "constant"
class Demo:
x = Label()
No comments yet. Be the first to comment!