Decorators & Function Wrappers
Master metaprogramming and dynamic function wrapping in Python. Understand lexical closures, first-class function mechanics, the @ syntax desugaring, signature-agnostic wrapping with *args and **kwargs, metadata preservation with functools.wraps, decorator stacking order, parameterized decorator factories, and class-based decorators.
Prerequisites & Prior Knowledge
Core Mastery Goals
- •Deconstruct the @ decorator syntax as first-class higher-order function composition: func = dec(func)
- •Construct robust, signature-agnostic wrapper functions using *args, **kwargs, and return value forwarding
- •Preserve function identity, docstrings, annotations, and unwrap references using functools.wraps
First-Class Functions, Closures, and the @ Syntax Desugaring
In Python, functions are first-class citizens: they can be passed as arguments, assigned to variables, and returned from other functions. A closure is an inner function that retains access to variables from its enclosing lexical scope even after the outer function has finished executing.
def announce(func):
def wrapper():
print(">> [PRE] Execution started")
result = func()
print(">> [POST] Execution completed")
return result
return wrapper
@announce
def execute_task():
print("Processing batch records...")
return "SUCCESS"
status = execute_task()
print("Final status:", status)Signature-Agnostic Wrappers with *args and **kwargs
Real-world decorators must wrap any function regardless of its positional and keyword parameters. Always define the inner wrapper as def wrapper(*args, **kwargs):, forward those arguments when calling func(*args, **kwargs), and explicitly return the computed result.
Captures all arbitrary positional arguments into a tuple, forwarding them positionally to the target.
Captures all arbitrary keyword arguments into a dictionary, forwarding them unpacked to the target.
Never swallow the return value! Always store and return what func() produces.
Preserving Function Identity with functools.wraps
When a function is wrapped, the variable name now points to the inner wrapper function. Without intervention, critical introspection attributes (like __name__ and __doc__) are wiped out, breaking Sphinx doc generators, unit test runners, and IDE autocompletion.
Placing @functools.wraps(func) above the inner wrapper copies __name__, __doc__,__module__, and __annotations__, while also attaching a __wrapped__ attribute that allows programmatic retrieval of the original function for unit testing!
import functools
def logger(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@logger
def calculate_discount(price: float, discount_pct: float = 0.1) -> float:
"""Compute discounted price rounded to 2 decimals."""
return round(price * (1.0 - discount_pct), 2)
final_price = calculate_discount(100.0, discount_pct=0.15)
print("Final price:", final_price)
print("Function name preserved:", calculate_discount.__name__)
print("Docstring preserved:", calculate_discount.__doc__)
print("Original unwrapped function exists:", hasattr(calculate_discount, '__wrapped__'))Decorator Stacking: Definition-Time vs Call-Time Order
Decorators are applied from the inside out:compute = dec1(dec2(compute))dec2 wraps the core target first, then dec1 wraps dec2.
When compute() is invoked:dec1 runs first, calls dec2, which runs and calls the core function. Return values bubble back up in reverse!
import functools
def make_bold(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def make_italic(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
@make_bold
@make_italic
def format_message(msg: str) -> str:
return msg
print(format_message("System Operational"))Interactive Decorator Stacking Call-Stack Visualizer
Toggle and reorder decorators to inspect the exact nested execution layers and output string wrapping.
Parameterized Decorators (The 3-Tier Factory Architecture)
When a decorator needs configuration arguments (e.g. @repeat(num_times=3)), the standard two-layer decorator signature cannot accept them. You must construct a decorator factory consisting of three nested closures.
Accepts custom parameters (e.g. repeat(3)) and returns the actual decorator callable.
Receives the target function to decorate, applying @functools.wraps(func).
Intercepts calls at runtime, using both config and func from outer closure scopes.
import functools
def repeat(num_times: int):
"""Decorator factory that repeats function execution N times."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
results = [func(*args, **kwargs) for _ in range(num_times)]
return results
return wrapper
return decorator
@repeat(num_times=3)
def roll_dice():
import random
return random.randint(1, 6)
import random; random.seed(42)
print("Repeated roll results:", roll_dice())Configurable Decorator Factory Sandbox
Modify factory arguments dynamically to observe how configuration propagates through closures into output logs.
Class-Based Decorators & State Management
When a decorator needs to maintain internal state across multiple calls (such as hit counts, caches, or circuit breakers), writing a Python class implementing the __call__ dunder method is cleaner and more object-oriented than function closures.
import functools
class CallCounter:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"[METRICS] {self.func.__name__} invocation #{self.call_count}")
return self.func(*args, **kwargs)
@CallCounter
def process_transaction(amount: float):
return f"Processed $${amount:.2f}"
print(process_transaction(45.0))
print(process_transaction(99.5))
print("Total tracked calls:", process_transaction.call_count)The callable() Built-In & Type Verification
The built-in callable(object) returns True if the object supports the call syntax (). This includes functions, lambdas, classes, and class instances defining __call__.
class Action:
def __call__(self):
return "Action performed"
item1 = lambda x: x * 2
item2 = Action()
item3 = [1, 2, 3]
print("Is lambda callable?", callable(item1))
print("Is Action instance callable?", callable(item2))
print("Is list callable?", callable(item3))Tiered Code Walkthroughs
Basic Closure and Wrapper Anatomy
The wrapper intercepts execution, prints telemetry markers before and after calling the original function, and returns the computed result back to the caller.
def announce(func):
def wrapper():
print(">> [PRE] Execution started")
result = func()
print(">> [POST] Execution completed")
return result
return wrapper
@announce
def execute_task():
print("Processing batch records...")
return "SUCCESS"
status = execute_task()
print("Final status:", status)Generic Wrapper with functools.wraps
@functools.wraps copies __name__, __doc__, and attaches __wrapped__, ensuring reflection, IDE autocomplete, and debugging tools see the true target function.
import functools
def logger(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print(f"[LOG] Calling {func.__name__} with args={args}, kwargs={kwargs}")
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} returned {result}")
return result
return wrapper
@logger
def calculate_discount(price: float, discount_pct: float = 0.1) -> float:
"""Compute discounted price rounded to 2 decimals."""
return round(price * (1.0 - discount_pct), 2)
final_price = calculate_discount(100.0, discount_pct=0.15)
print("Final price:", final_price)
print("Function name preserved:", calculate_discount.__name__)
print("Docstring preserved:", calculate_discount.__doc__)
print("Original unwrapped function exists:", hasattr(calculate_discount, '__wrapped__'))Inspecting Callables and Validating Types
callable() checks whether an object can be called with parentheses, making it essential for defensive framework and decorator programming.
class Action:
def __call__(self):
return "Action performed"
item1 = lambda x: x * 2
item2 = Action()
item3 = [1, 2, 3]
print("Is lambda callable?", callable(item1))
print("Is Action instance callable?", callable(item2))
print("Is list callable?", callable(item3))Common Traps & Decorator Anti-Patterns
Syntax or Name Typos in Decorators & Function Wrappers
# Mistyped variable or missing closing parenthesis result = decorators___function_wrappers_val
# Declare and verify variables before execution
val = 10
print("Defined correctly:", val)Unintended State Mutation or Scope Leakage
# Modifying shared state or unexpected variable shadowing
def update_state(data=[]):
data.append(1)
return datadef update_state(data=None):
if data is None:
data = []
data.append(1)
return dataResource Leaks & High-Scale Performance Regressions
# Opening resources in a tight loop without closing or bounding memory
files = [open(f"log_{i}.txt", "w") for i in range(5000)]for i in range(5):
with open(f"log_{i}.txt", "w") as f:
f.write("Safe")Real-World Architecture & Scale
🟢 Simple Script: Local Developer Automation Tool
Individual developers and IT administrators automate everyday repetitive workflows.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Calling: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def say_hi():
print("Hi!")
say_hi()🟡 Scaled Service: Backend API Service Integration
SaaS platforms run backend microservices processing requests concurrently.
# Service integration layer
def handle_request(payload: dict) -> dict:
return {"status": "success", "processed": True}🔴 Enterprise Production: High-Throughput Cloud Architecture
Web frameworks (FastAPI, Flask) use decorators for route declarations, authentication enforcement, and distributed tracing spans.
# High-resilience production pattern
import time
def process_stream(events: list) -> int:
processed = 0
for event in events:
if event:
processed += 1
return processedComprehensive Self-Check Quiz Booster
Test your understanding across all 16 conceptual, syntax, and output-prediction questions.
def add_star(func):
def wrapper():
return "*" + func() + "*"
return wrapper
@add_star
def hello():
return "hi"
print(hello())def double_res(f):
return lambda x: f(x) * 2
@double_res
def square(n):
return n * n
print(square(3))def logged(fn):
def inner(a, b):
return fn(a, b) + 1
return inner
@logged
def add(a, b):
return a + b
print(add(2, 3))def shout(fn):
def wrap(msg):
return fn(msg).upper()
return wrap
@shout
def greet(name):
return f"hello {name}"
print(greet("ada"))def tag(fn):
def wrap():
return f"{fn()}"
return wrap
@tag
def text():
return "test"
print(text())Guided Practice Challenge
ADMINWrite a decorator uppercase_result(func) that takes a function returning a string and returns its output converted to uppercase with .upper(). Apply it to @uppercase_result def get_role(): return 'admin' and print get_role().
import functools # Define uppercase_result decorator # Apply to get_role() and print get_role()
Decorator Overhead & Stack Depth
Each decorator adds exactly one Python call frame to the call stack during execution.
The wrapper function is instantiated only once when the module loads, not on every function call.
@functools.lru_cache turns exponential recursive algorithms into instant hash lookups.
Key Takeaways & Architectural Cheatsheet
Lesson 05: Generators, Iterators & Yield Expressions
Master the Python iterator protocol (__iter__, __next__), infinite streams, and memory-efficient streaming pipelines.
Worked example
Log function execution with a reusable decorator.
import functools
def log(f):
@functools.wraps(f)
def w(): print("run"); return f()
return w
@log
def ping(): print("pong")
ping()
How it works, step by step
- @log intercepts the call to ping().
Decorators & Function Wrappers practice
Write a decorator uppercase_result(func) that takes a function returning a string and returns its output converted to uppercase with .upper(). Apply it to @uppercase_result def get_role(): return 'admin' and print get_role().
Coding Practice is Locked
Sign in to access the interactive Python editor, write and run code checks against automated tests, use hints, and earn XP.
Practice quiz
Check yourself
15 questions with explanations after submission, so you can correct course as you go.
Decorators & Function Wrappers quiz
LockedYou must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.
Lesson incomplete
Not completedCreate an ID or sign in to save lesson completion across devices.