Python Mastery · Chapter 12 · ~25 min · Advanced
Decorators, Protocols & Metaprogramming
Unlock the architectural patterns that power industry frameworks like FastAPI, Pydantic, and SQLAlchemy: higher-order decorators with functools.wraps, static duck-typing with typing.Protocol, descriptor attribute interception, and generator context managers.
- See that @dec means fn = dec(fn)
- Keep metadata with functools.wraps
- Use Protocol for typed duck typing
- Write context managers with contextlib
1Decorators & Middleware Wrapping
Decorators are functions that take another function as an argument, augment its behavior, and return a new function. They are the foundation of authentication guards, caching layers, rate limiters, and telemetry tracking in modern web frameworks.
Decorator Wrapping & Middleware Pipeline
Decorators wrap functions like layers of an onion. Watch execution traverse down and return back up.
Click "Invoke Function" to trace execution flow.
@wraps(fn).Mental model
The one formula that makes decorators click
The @ symbol is just shorthand. These two are exactly the same:
@log_calls def greet(): ... # is EXACTLY: def greet(): ... greet = log_calls(greet)
So a decorator is simply a function that takes your function and returns a new (wrapper) function.
Without @functools.wraps, what does greet.__name__ show?
2Duck Typing with Types: typing.Protocol
Python's historic motto has always been "If it walks like a duck and quacks like a duck, it is a duck". With PEP 544 (typing.Protocol), Python introduced structural subtyping. You can define an interface, and any class that implements the required methods satisfies the type checker—without needing to inherit from a rigid base class!
typing.Protocol & Structural Subtyping (Duck Typing with Types)
Static duck typing: If an object implements the protocol methods, Mypy accepts it without rigid inheritance hierarchies.
from typing import Protocol
class StorageBackend(Protocol):
"""Any object with these 3 methods satisfies this type!"""
def read(self, key: str) -> bytes: ...
def write(self, key: str, data: bytes) -> None: ...
def delete(self, key: str) -> None: ...
def backup_database(storage: StorageBackend) -> None:
# Completely decoupled from concrete AWS or disk classes!
storage.write("backup.sql", b"DATA")abc.ABC, no coupled inheritance. Perfect for clean plugin architectures.class S3BlobStorage:
# Notice: ZERO inheritance from StorageBackend!
def read(self, key: str) -> bytes:
return s3_client.get_object(Key=key)["Body"].read()
def write(self, key: str, data: bytes) -> None:
s3_client.put_object(Key=key, Body=data)
def delete(self, key: str) -> None:
s3_client.delete_object(Key=key)✔ Satisfies StorageBackend because it implements read, write, and delete.
Nominal Typing (Rigid abc.ABC):
from abc import ABC, abstractmethod
class BaseStorage(ABC):
@abstractmethod
def read(self): ...
# MUST explicitly inherit:
class S3(BaseStorage):
def read(self): ...Structural Typing (Flexible Protocol):
from typing import Protocol
class Storage(Protocol):
def read(self) -> bytes: ...
# Zero inheritance required!
class S3:
def read(self) -> bytes: ...A class has a .read() method but never inherits from Readable(Protocol). Does it match?
3Descriptors: The Engine Behind Properties & ORMs
Have you ever wondered how @property works, or how Pydantic validates fields, or how SQLAlchemy maps database columns to class attributes? The answer is Descriptors: objects that define any of __get__, __set__, or __set_name__.
Descriptors: How @property, Pydantic & ORMs Intercept Attributes
A descriptor is an object that implements __get__, __set__, and __set_name__.
class PositiveInt:
def __set_name__(self, owner, name):
self.storage_name = f"_{name}"
def __get__(self, instance, owner):
if instance is None:
return self
return getattr(instance, self.storage_name, 0)
def __set__(self, instance, value):
if not isinstance(value, int) or value <= 0:
raise ValueError(f"'{self.storage_name}' must be > 0")
setattr(instance, self.storage_name, value)
class Product:
price = PositiveInt() # Descriptor instance on classDescriptor Validation Passed
Value 25 written to product.__dict__["_price"]
@property, Django model field, and Pydantic validator in Python.4Lightweight Context Managers with contextlib
Writing a class with __enter__ and __exit__ can be verbose. The standard library's @contextlib.contextmanager decorator allows you to turn a simple generator function into a bulletproof context manager using a try / yield / finally block.
Writing Custom Context Managers with @contextlib.contextmanager
Turn any generator function into an automated resource manager with zero boilerplate.
from contextlib import contextmanager
@contextmanager
def db_transaction(db_pool):
# 1. SETUP (Before yield)
conn = db_pool.acquire()
print("BEGIN TRANSACTION")
try:
# 2. YIELD to with body
yield conn
# 4a. SUCCESS COMMIT
conn.commit()
except Exception:
# 4b. ROLLBACK ON ERROR
conn.rollback()
raise
finally:
# 4c. GUARANTEED CLEANUP
conn.close()1. Setup (__enter__)
Code before 'yield' executes: Acquiring database connection & starting transaction.
2. Yield Resource
Resource passed to variable: 'with db_transaction() as tx:'.
3. User Code Executes
Operations inside the with block: tx.insert_order(order_data).
4. Cleanup (__exit__ / finally)
Code after 'yield' executes: Committing transaction & closing socket connection!
The finally block guarantees database connection release even if code inside with raises a fatal exception.
5The 4 Fatal Traps of Python Metaprogramming
Metaprogramming gives you the ability to reshape Python's syntax and execution model. Review the four most common pitfalls before building framework-level code.
The 4 Fatal Traps of Advanced Metaprogramming
Metaprogramming gives you immense power. Avoid these four subtle traps that cause bizarre shared-state bugs and silent failures.
Forgetting @functools.wraps on Decorators
Without @functools.wraps, decorators erase the original function's name, docstrings, and type annotations, breaking debuggers, logging libraries, and FastAPI endpoint routers.
def my_decorator(fn):
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
@my_decorator
def calculate_vat():
"""Calculates EU VAT."""
pass
print(calculate_vat.__name__) # Prints 'wrapper'!
print(calculate_vat.__doc__) # Prints None! Breaks documentation generators!from functools import wraps
def my_decorator(fn):
@wraps(fn) # Copies __name__, __doc__, and annotations!
def wrapper(*args, **kwargs):
return fn(*args, **kwargs)
return wrapper
print(calculate_vat.__name__) # 'calculate_vat'
print(calculate_vat.__doc__) # 'Calculates EU VAT.'6Capstone Challenge: Python Mastery Architect
Test your understanding of decorator lifecycles, structural protocols, descriptor attribute binding, and contextlib exception handling. Score 5/5 to complete the entire Python Mastery Series!
Decorators, Protocols & Metaprogramming · code lab
Edit the example, run it with Python, or step through the lines.
1def announce(func):2 def wrapper(*args, **kwargs):3 print("Calling", func.__name__)4 return func(*args, **kwargs)5 return wrapper67@announce8def greet(name):9 return f"Hello, {name}!"1011print(greet("Ada"))
Names
Objects in memory
Python output
Run the code to see its output.
Advanced Python Capstone Challenge
Pick an answer — you get feedback instantly. Get every question right to finish the chapter.
1Why should custom decorators almost always use @functools.wraps(fn)?
2When does the outer code of a decorator function execute in Python?
3What is the key difference between typing.Protocol and an abstract base class (abc.ABC)?
4Why does Python 3.6+ provide __set_name__(self, owner, name) for descriptors?
5In a @contextmanager generator, what happens if an exception is raised inside the 'with' block?
Run and experiment with the full metaprogramming framework in the Python Playground (code preloaded).
7Recap & Course Completion
- Higher-Order Decorators: Wrap function execution cleanly; always decorate your inner wrapper with
@functools.wraps. - Import vs Call Time: Remember that outer decorator code runs once on import; wrapper code runs on invocation.
- typing.Protocol: Leverage modern static duck typing to decouple interfaces from concrete implementations.
- Descriptors: Use
__get__,__set__, and__set_name__to build reusable attribute validators and ORM fields. - @contextmanager: Use generators with
try ... yield ... finallyto guarantee resource cleanup.
Finish the capstone challenge above to complete Chapter 12 and the entire course!
Score 5/5 or click below to manually mark the capstone complete.