Python28 min read

Python Descriptors

Learn descriptors (the engine behind properties): define __get__ and __set__ to control attribute access, validation, and caching.

David Miller
August 16, 2025
11.6k451

Descriptors are a low-level feature that powers many high-level tools in Python.

      Important fact:
      - `@property` is implemented using descriptors.
      
      A descriptor is any object that defines:
      - __get__
      - __set__
      - __delete__ (optional)
      
      ## Example: Positive-only descriptor
      
      ```python
      class Positive:
          def __init__(self, name):
              self.name = name
      
          def __get__(self, obj, objtype=None):
              return obj.__dict__.get(self.name, 0)
      
          def __set__(self, obj, value):
              if value < 0:
                  raise ValueError("Value must be positive")
              obj.__dict__[self.name] = value
      
      class Account:
          balance = Positive("balance")
      
          def __init__(self, balance):
              self.balance = balance
      
      acc = Account(1000)
      print(acc.balance)
      # acc.balance = -50  # error
      ```
      
      ## Example: type validation descriptor
      
      ```python
      class Typed:
          def __init__(self, name, expected_type):
              self.name = name
              self.expected_type = expected_type
      
          def __get__(self, obj, objtype=None):
              return obj.__dict__.get(self.name)
      
          def __set__(self, obj, value):
              if not isinstance(value, self.expected_type):
                  raise TypeError(f"Expected {self.expected_type.__name__}")
              obj.__dict__[self.name] = value
      
      class Person:
          name = Typed("name", str)
          age = Typed("age", int)
      
          def __init__(self, name, age):
              self.name = name
              self.age = age
      
      p = Person("Tom", 25)
      print(p.name, p.age)
      ```
      
      ## Example: caching descriptor (simple cached property)
      
      ```python
      class CachedProperty:
          def __init__(self, func):
              self.func = func
              self.name = func.__name__
      
          def __get__(self, obj, objtype=None):
              if obj is None:
                  return self
              if self.name in obj.__dict__:
                  return obj.__dict__[self.name]
              value = self.func(obj)
              obj.__dict__[self.name] = value
              return value
      
      class DataProcessor:
          @CachedProperty
          def expensive(self):
              print("Computing...")
              return sum(range(1_000_000))
      
      dp = DataProcessor()
      print(dp.expensive)
      print(dp.expensive)
      ```
      
      Expected output:
      ```
      Computing...
      499999500000
      499999500000
      ```
      
      ## Graph: descriptor role
      
      ```mermaid
      flowchart LR
        A[Access obj.attr] --> B[Descriptor __get__]
        C[Assign obj.attr = x] --> D[Descriptor __set__]
        B --> E[Return value]
        D --> F[Validate + store]
      ```
      
      ## Key points
      
      - Descriptors control attribute access deeply
      - Properties are built on descriptors
      - Powerful but advanced, use only when needed
      
#Python#Advanced#OOP