Descriptors
Python · Advanced · Run with python3
Explanation
A descriptor is an object that defines `__get__`, `__set__`, or `__delete__`. When stored as a class attribute, Python calls these methods instead of the normal attribute access. This is how `@property`, `@classmethod`, `@staticmethod`, and all ORM fields work. Data descriptors define both `__get__` and `__set__`; non-data descriptors define only `__get__`. Python's attribute lookup order: data descriptors > instance dict > non-data descriptors.
Real-World Scenario
A `Validated` descriptor enforces type and range constraints on model fields. One descriptor class handles validation for all fields — add `age = Validated(int, min=0, max=150)` to any class and it automatically validates on assignment. ```python class Typed: """Descriptor that enforces a type constraint.""" def __set_name__(self, owner, name: str) -> None: self._name = f"_{name}" self._public = name def __init__(self, expected_type: type): self._type = expected_type def __get__(self, obj, objtype=None): if obj is None: return self # Class-level access returns the descriptor itself return getattr(obj, self._name, None) def __set__(self, obj, value) -> None: if not isinstance(value, self._type): raise TypeError(f"{self._public} must be {self._type.__name__}, got {type(value).__name__}") setattr(obj, self._name, value) class Person: name = Typed(str) age = Typed(int) def __init__(self, name: str, age: int): self.name = name self.age = age p = Person("Alice", 30) print(p.name, p.age) # Alice 30 try: p.age = "thirty" except TypeError as e: print(e) # age must be int, got str ```
Quick Snippet
class Typed:
"""Descriptor that enforces a type constraint."""
def __set_name__(self, owner, name: str) -> None:
self._name = f"_{name}"
self._public = name
def __init__(self, expected_type: type):
self._type = expected_type
def __get__(self, obj, objtype=None):
if obj is None:
return self # Class-level access returns the descriptor itself
return getattr(obj, self._name, None)
def __set__(self, obj, value) -> None:
if not isinstance(value, self._type):
raise TypeError(f"{self._public} must be {self._type.__name__}, got {type(value).__name__}")
setattr(obj, self._name, value)
class Person:
name = Typed(str)
age = Typed(int)
def __init__(self, name: str, age: int):
self.name = name
self.age = age
p = Person("Alice", 30)
print(p.name, p.age) # Alice 30
try:
p.age = "thirty"
except TypeError as e:
print(e) # age must be int, got strFull Example
# Compile: python3 descriptors.py
# Output:
# Alice 30
# age must be int, got str
class Typed:
def __set_name__(self, owner, name: str) -> None:
self._name = f"_{name}"
self._public = name
def __init__(self, expected_type: type):
self._type = expected_type
def __get__(self, obj, objtype=None):
if obj is None:
return self
return getattr(obj, self._name, None)
def __set__(self, obj, value) -> None:
if not isinstance(value, self._type):
raise TypeError(
f"{self._public} must be {self._type.__name__}, "
f"got {type(value).__name__}"
)
setattr(obj, self._name, value)
class Person:
name = Typed(str)
age = Typed(int)
def __init__(self, name: str, age: int):
self.name = name
self.age = age
p = Person("Alice", 30)
print(p.name, p.age)
try:
p.age = "thirty"
except TypeError as e:
print(e)
# Class-level access returns the descriptor itself
print(type(Person.name)) # <class '__main__.Typed'>
python3 descriptors.py