2. OOP Advanced: Descriptors, Properties, Metaclasses

Leverage Python's object model to build concise, powerful abstractions with explicit control over attribute access and class creation.

Question: What is the descriptor protocol in Python?

Answer: The descriptor protocol allows an object's attribute access to be customized. An object that implements any of the __get__, __set__, or __delete__ methods is considered a descriptor and can control the behavior of attributes in an owner class.

Explanation: This is a powerful, low-level feature that underpins much of Python's object model. Properties, methods, @staticmethod, and @classmethod are all implemented using descriptors. It's the mechanism that allows attribute access to do more than just retrieve a value from a dictionary.

class Positive:
    def __set_name__(self, owner, name):
        self.name = name
    def __get__(self, obj, owner=None):
        if obj is None: return self
        return obj.__dict__[self.name]
    def __set__(self, obj, value):
        if value <= 0: raise ValueError("must be positive")
        obj.__dict__[self.name] = value

class Product:
    price = Positive()
    def __init__(self, price: float):
        self.price = price

Question: What is a metaclass in Python and what is a practical use case?

Answer: A metaclass is the "class of a class"; it defines how a class is created. While you would rarely need to create a custom metaclass in application code, it's a powerful tool used in frameworks to implement patterns like registration, validation, or ORM model creation.

Explanation: When you define a class, Python uses a metaclass (by default, type) to create that class object. By creating a custom metaclass, you can intercept the class creation process to automatically modify the class. ORMs like SQLAlchemy and Django use metaclasses extensively to create database models from class definitions.

class Registry(type):
    registry = {}
    def __new__(mcls, name, bases, ns):
        cls = super().__new__(mcls, name, bases, ns)
        if name != "Base":
            mcls.registry[name] = cls
        return cls

class Base(metaclass=Registry):
    pass

class Foo(Base):
    pass
# Registry.registry => {"Foo": <class Foo>}

Question: How would you implement a decorator from scratch, and what is the purpose of functools.wraps?

Answer: A decorator is a function that takes another function, wraps it in an inner function to add functionality, and returns the inner function. functools.wraps is a helper decorator that should be applied to the inner function to preserve the original function's metadata (like its name and docstring).

Explanation: Without @wraps, the decorated function would lose its original identity, which can cause issues with introspection and debugging. The example below shows a simple timing decorator.

from functools import wraps
import time

def timing(fn):
    @wraps(fn)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        try:
            return fn(*args, **kwargs)
        finally:
            end = time.perf_counter()
            print(f"{fn.__name__} took {end - start:.4f}s")
    return wrapper

@timing
def do_work():
    time.sleep(0.1)

Question: How do abstract base classes (ABCs) compare to Protocols, and when use each?

Answer: ABCs enforce nominal subtyping via inheritance and can include default behavior. Protocols enable structural typing: any class with the right shape matches.

Explanation: Use ABCs when you need shared implementation or isinstance checks without @runtime_checkable. Use Protocol for flexible libraries that accept any conforming type.

from abc import ABC, abstractmethod

class Repo(ABC):
    @abstractmethod
    def get(self, key: str) -> str: ...

Question: When should you use @property vs functools.cached_property?

Answer: Use @property for cheap, computed attributes. Use cached_property for expensive, immutable computations that should be evaluated once per instance.

Explanation: cached_property caches the value; clear or recompute when underlying state changes.

from functools import cached_property

class Report:
    def __init__(self, data): self.data = data
    @cached_property
    def summary(self):
        return expensive_reduce(self.data)

Question: What is __init_subclass__ and a practical use?

Answer: __init_subclass__ is a hook called on a base whenever a subclass is created, letting you enforce or register things at subclass definition time.

Explanation: Useful for auto-registration, validation, or enforcing required attributes.

class Plugin:
    registry = {}
    def __init_subclass__(cls, name: str, **kw):
        super().__init_subclass__(**kw)
        Plugin.registry[name] = cls

class CSVPlugin(Plugin, name="csv"): pass
# Plugin.registry["csv"] is CSVPlugin