CodeNebula
Python🚀Advancedadvanced

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

snippet.py
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

Full Example

descriptors.py
# 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'>
Run command
python3 descriptors.py