Skip to content

Descriptor Use Cases

Validation

1. Type Checking

class TypedAttribute:
    def __init__(self, name, expected_type):
        self.name = name
        self.expected_type = expected_type

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not isinstance(value, self.expected_type):
            raise TypeError(
                f"{self.name} must be {self.expected_type.__name__}"
            )
        instance.__dict__[self.name] = value

class Person:
    name = TypedAttribute('name', str)
    age = TypedAttribute('age', int)
    height = TypedAttribute('height', (int, float))

p = Person()
p.name = "Alice"  # ✅ OK
p.age = 30        # ✅ OK
# p.age = "30"    # ❌ TypeError

2. Range Validation

class BoundedNumber:
    def __init__(self, name, min_value=None, max_value=None):
        self.name = name
        self.min_value = min_value
        self.max_value = max_value

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if self.min_value is not None and value < self.min_value:
            raise ValueError(f"{self.name} must be >= {self.min_value}")
        if self.max_value is not None and value > self.max_value:
            raise ValueError(f"{self.name} must be <= {self.max_value}")
        instance.__dict__[self.name] = value

class Temperature:
    celsius = BoundedNumber('celsius', min_value=-273.15)
    fahrenheit = BoundedNumber('fahrenheit', min_value=-459.67)

t = Temperature()
t.celsius = 25    # ✅ OK
# t.celsius = -300  # ❌ ValueError

3. Pattern Validation

import re

class RegexValidated:
    def __init__(self, name, pattern):
        self.name = name
        self.pattern = re.compile(pattern)

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if not self.pattern.match(value):
            raise ValueError(
                f"{self.name} must match pattern: {self.pattern.pattern}"
            )
        instance.__dict__[self.name] = value

class User:
    email = RegexValidated('email', r'^[\w\.-]+@[\w\.-]+\.\w+$')
    phone = RegexValidated('phone', r'^\d{3}-\d{3}-\d{4}$')

user = User()
user.email = "alice@example.com"  # ✅ OK
# user.email = "invalid"          # ❌ ValueError

ORM Patterns

1. Database Field

class Field:
    def __init__(self, field_type, required=False, default=None):
        self.field_type = field_type
        self.required = required
        self.default = default
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name, self.default)

    def __set__(self, instance, value):
        if value is None and self.required:
            raise ValueError(f"{self.name} is required")
        if value is not None and not isinstance(value, self.field_type):
            raise TypeError(f"{self.name} must be {self.field_type.__name__}")
        instance.__dict__[self.name] = value

class Model:
    def save(self):
        data = {}
        for name, field in type(self).__dict__.items():
            if isinstance(field, Field):
                value = getattr(self, name)
                if value is not None:
                    data[name] = value
        print(f"Saving to database: {data}")
        return data

class User(Model):
    username = Field(str, required=True)
    email = Field(str, required=True)
    age = Field(int, default=0)

user = User()
user.username = "alice"
user.email = "alice@example.com"
user.age = 30
user.save()  # Saving to database: {'username': 'alice', 'email': 'alice@example.com', 'age': 30}

2. Foreign Key

class ForeignKey:
    def __init__(self, model_class):
        self.model_class = model_class
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        fk_id = instance.__dict__.get(f"{self.name}_id")
        if fk_id is not None:
            # Simulate database lookup
            return self.model_class.get(fk_id)
        return None

    def __set__(self, instance, value):
        if not isinstance(value, self.model_class):
            raise TypeError(f"Must be instance of {self.model_class.__name__}")
        instance.__dict__[f"{self.name}_id"] = value.id

class Department:
    def __init__(self, id, name):
        self.id = id
        self.name = name

    @staticmethod
    def get(id):
        # Simulate database lookup
        return Department(id, f"Department {id}")

class Employee:
    department = ForeignKey(Department)

dept = Department(1, "Engineering")
emp = Employee()
emp.department = dept

3. Lazy Relationship

class LazyRelationship:
    def __init__(self, model_class, foreign_key):
        self.model_class = model_class
        self.foreign_key = foreign_key
        self.name = None

    def __set_name__(self, owner, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self

        # Check cache
        cache_key = f"_{self.name}_cache"
        if cache_key not in instance.__dict__:
            # Load from database
            fk_value = getattr(instance, self.foreign_key)
            instance.__dict__[cache_key] = self.model_class.query(fk_value)

        return instance.__dict__[cache_key]

Performance Optimization

1. Lazy Loading

class LazyProperty:
    def __init__(self, func):
        self.func = func
        self.name = func.__name__

    def __get__(self, instance, owner):
        if instance is None:
            return self

        # Load once and cache
        value = self.func(instance)
        instance.__dict__[self.name] = value
        return value

class DataProcessor:
    def __init__(self, filename):
        self.filename = filename

    @LazyProperty
    def data(self):
        print(f"Loading {self.filename}...")
        # Expensive I/O operation
        with open(self.filename) as f:
            return f.read()

processor = DataProcessor('data.txt')
# Not loaded yet
print(processor.data)  # Loading data.txt...
print(processor.data)  # No loading (cached)

2. Memoization

class Memoized:
    def __init__(self, func):
        self.func = func
        self.cache = {}

    def __get__(self, instance, owner):
        if instance is None:
            return self

        def memoized_func(*args):
            if args not in self.cache:
                self.cache[args] = self.func(instance, *args)
            return self.cache[args]

        return memoized_func

class Calculator:
    @Memoized
    def fibonacci(self, n):
        print(f"Computing fib({n})")
        if n <= 1:
            return n
        return self.fibonacci(n-1) + self.fibonacci(n-2)

calc = Calculator()
print(calc.fibonacci(5))  # Computes each value once
print(calc.fibonacci(5))  # All from cache

3. Weak References

import weakref

class WeakAttribute:
    def __init__(self):
        self.data = weakref.WeakValueDictionary()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return self.data.get(id(instance))

    def __set__(self, instance, value):
        self.data[id(instance)] = value

class Node:
    parent = WeakAttribute()

Access Control

1. Read-Only After Init

class ReadOnlyAfterInit:
    def __init__(self, name):
        self.name = name
        self.initialized = weakref.WeakSet()

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if instance in self.initialized:
            raise AttributeError(f"{self.name} is read-only after initialization")
        instance.__dict__[self.name] = value
        self.initialized.add(instance)

class Config:
    api_key = ReadOnlyAfterInit('api_key')

config = Config()
config.api_key = "secret123"  # ✅ OK (first time)
# config.api_key = "new"      # ❌ AttributeError (read-only)

2. Permission-Based Access

class PermissionRequired:
    def __init__(self, name, read_perm=None, write_perm=None):
        self.name = name
        self.read_perm = read_perm
        self.write_perm = write_perm

    def __get__(self, instance, owner):
        if instance is None:
            return self
        if self.read_perm and not instance.has_permission(self.read_perm):
            raise PermissionError(f"No read access to {self.name}")
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        if self.write_perm and not instance.has_permission(self.write_perm):
            raise PermissionError(f"No write access to {self.name}")
        instance.__dict__[self.name] = value

3. Audit Trail

from datetime import datetime

class AuditedAttribute:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        # Log change
        if not hasattr(instance, '_audit_log'):
            instance._audit_log = []

        old_value = instance.__dict__.get(self.name)
        instance._audit_log.append({
            'field': self.name,
            'old': old_value,
            'new': value,
            'timestamp': datetime.now()
        })

        instance.__dict__[self.name] = value

class AuditedModel:
    name = AuditedAttribute('name')
    value = AuditedAttribute('value')

model = AuditedModel()
model.name = "Alice"
model.value = 100
model.value = 200
print(model._audit_log)

Type Conversion

1. Auto-Converting

class AutoConvert:
    def __init__(self, name, converter):
        self.name = name
        self.converter = converter

    def __get__(self, instance, owner):
        if instance is None:
            return self
        return instance.__dict__.get(self.name)

    def __set__(self, instance, value):
        instance.__dict__[self.name] = self.converter(value)

class DataRecord:
    count = AutoConvert('count', int)
    price = AutoConvert('price', float)
    active = AutoConvert('active', bool)

record = DataRecord()
record.count = "42"      # Stored as int(42)
record.price = "19.99"   # Stored as float(19.99)
record.active = "yes"    # Stored as bool("yes") = True

2. JSON Serialization

import json

class JSONField:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        json_str = instance.__dict__.get(self.name)
        return json.loads(json_str) if json_str else None

    def __set__(self, instance, value):
        instance.__dict__[self.name] = json.dumps(value)

class Config:
    settings = JSONField('settings')

config = Config()
config.settings = {'debug': True, 'port': 8080}
print(config.settings)  # {'debug': True, 'port': 8080}

3. Unit Conversion

class Temperature:
    def __init__(self, name):
        self.name = name

    def __get__(self, instance, owner):
        if instance is None:
            return self
        # Stored in Celsius
        return instance.__dict__.get(self.name, 0)

    def __set__(self, instance, value):
        # Value can be dict with unit
        if isinstance(value, dict):
            if value.get('unit') == 'F':
                # Convert F to C
                celsius = (value['value'] - 32) * 5/9
                instance.__dict__[self.name] = celsius
            else:
                instance.__dict__[self.name] = value['value']
        else:
            instance.__dict__[self.name] = value

class Weather:
    temp = Temperature('temp')

weather = Weather()
weather.temp = {'value': 77, 'unit': 'F'}
print(weather.temp)  # 25.0 (Celsius)