Decorators, Abstract Base Classes & Protocols

Master Python interface contracts: higher-order decorator mechanics, functools.wraps metadata preservation, Abstract Base Classes, and PEP 544 structural Protocols.

03. Object-Oriented Systems, Descriptor Protocol & Metaprogramming8 min read

Designing maintainable, decoupled Python architectures requires expressive structural contracts. Python provides three complementary mechanisms for behavioral modification and interface enforcement:

  1. Decorators: Syntactic transformations wrapping functions or classes.
  2. Abstract Base Classes (ABCs): Nominal interface enforcement with runtime instantiation checks.
  3. Protocols (typing.Protocol): Static structural duck typing without explicit inheritance coupling.
Structural SubtypingType System
PEP 544

Enables static duck typing where classes satisfy interfaces purely by shape without subclassing.

1. Decorator Mechanics & Metadata Preservation#

A decorator @decorator is syntactic sugar evaluated at module load time. It passes the underlying function or class through a higher-order transformation function:

fdecorated=D(f)  ⟹  rebinds symbol f to the return value of D(f)f_{\text{decorated}} = D(f) \implies \text{rebinds symbol } f \text{ to the return value of } D(f)

When wrapping functions, the wrapper function must preserve original function metadata (such as __name__, __doc__, and __annotations__) using functools.wraps:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import functools import time def execution_timer(threshold_ms: float = 100.0): """Parameterized decorator logging slow function executions.""" def decorator(fn): @functools.wraps(fn) # Preserves __name__, docstrings, and signature! def wrapper(*args, **kwargs): start = time.perf_counter() result = fn(*args, **kwargs) duration_ms = (time.perf_counter() - start) * 1000.0 if duration_ms > threshold_ms: print(f"[SLOW EXECUTION] {fn.__name__} took {duration_ms:.2f}ms") return result return wrapper return decorator @execution_timer(threshold_ms=50.0) def process_data(records: list): """Processes customer records.""" time.sleep(0.06) return len(records) print(f"Function Name: {process_data.__name__}") # 'process_data' (NOT 'wrapper') print(f"Docstring: {process_data.__doc__}") # 'Processes customer records.'

2. Abstract Base Classes (ABCs)#

ABCs define nominal behavioral contracts using abc.ABCMeta. If a derived class fails to implement every method marked with @abstractmethod, CPython prevents instantiation:

Architecture Diagram
Rendering diagram...

Dynamic Virtual Subclassing via __subclasshook__#

ABCs can recognize classes as virtual subclasses even if they never inherit from the ABC! This is how Python built-in types satisfy collections.abc.Iterable and Mapping:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import abc class Serializable(abc.ABC): @classmethod def __subclasshook__(cls, subclass): # Dynamically checks if subclass implements serialize() if cls is Serializable: if any("serialize" in B.__dict__ for B in subclass.__mro__): return True return NotImplemented class UnrelatedReport: def serialize(self) -> str: return '{"report": "data"}' # Satisfies interface without explicit inheritance! print(f"isinstance: {isinstance(UnrelatedReport(), Serializable)}") # True print(f"issubclass: {issubclass(UnrelatedReport, Serializable)}") # True

3. Structural Typing: typing.Protocol (PEP 544)#

While ABCs rely on runtime checks, modern Python heavily leverages Structural Subtyping (Protocols) for static type checking.

A class satisfies a Protocol if it implements the required attributes and methods, regardless of its class inheritance tree:

∀mi∈Protocol:mi∈Class.Members  ⟹  Class≤structProtocol\forall m_i \in \text{Protocol}: \quad m_i \in \text{Class}.\text{Members} \implies \text{Class} \le_{\text{struct}} \text{Protocol}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from typing import Protocol, runtime_checkable @runtime_checkable class Renderable(Protocol): """Structural protocol requiring a render() method returning a string.""" def render(self) -> str: ... class HtmlWidget: def render(self) -> str: return "<div>Widget</div>" class SvgIcon: def render(self) -> str: return "<svg></svg>" def display_screen(component: Renderable): print(f"Rendering: {component.render()}") # Both classes satisfy Renderable with zero inheritance coupling! display_screen(HtmlWidget()) display_screen(SvgIcon()) # Runtime checkable verification print(f"HtmlWidget is Renderable: {isinstance(HtmlWidget(), Renderable)}") # True

4. Architectural Comparison Matrix#

MechanismCoupling TypeValidation TimingBest Use Case
Traditional InheritanceRigid NominalRuntimeSharing concrete code implementation and default behaviors across tightly bound domain entities.
Abstract Base ClassesNominal (Explicit)Runtime (__new__)Enforcing strict API contracts across internal framework plugins and service layers.
Protocols (Protocol)Decoupled StructuralStatic (Mypy) / RuntimeDefining composable, third-party friendly interfaces without forcing external libraries to subclass your classes.

5. Concept Verification#

Test your understanding of decorators, ABCs, and protocols:

Concept check

Why should developers always decorate wrapper functions with @functools.wraps(fn) inside custom decorators?

Concept check

What is the primary advantage of typing.Protocol over Abstract Base Classes (ABCs)?