Skip to contentSkip to navigation
Module 06 — Functional PythonLesson 04Intermediate / Advanced

Decorators & Function Wrappers

30 min study17 Deep Sections

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

`functions``scope-recursion`

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
Section 02/Foundations

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.

Syntactic Equivalence:
# Sugar Syntax:
@announce
def task(): pass
# Exact Desugared Equivalent:
def task(): pass
task = announce(task)
Python
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)
Section 03/Universal Signatures

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.

*args

Captures all arbitrary positional arguments into a tuple, forwarding them positionally to the target.

**kwargs

Captures all arbitrary keyword arguments into a dictionary, forwarding them unpacked to the target.

return result

Never swallow the return value! Always store and return what func() produces.

Section 04/Introspection Safety

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.

The @functools.wraps Fix:

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!

Python
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__'))
Section 05/Pipeline Execution

Decorator Stacking: Definition-Time vs Call-Time Order

1. Definition Phase (Bottom-Up)

Decorators are applied from the inside out:
compute = dec1(dec2(compute))
dec2 wraps the core target first, then dec1 wraps dec2.

2. Call Phase (Top-Down)

When compute() is invoked:
dec1 runs first, calls dec2, which runs and calls the core function. Return values bubble back up in reverse!

Python
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"))
Section 06/Interactive Simulator

Interactive Decorator Stacking Call-Stack Visualizer

Toggle and reorder decorators to inspect the exact nested execution layers and output string wrapping.

Configure Decorator Stack:
Top-Level Decorator:
Call Frame Propagation (Top to Bottom):
1. Call invoked: @logger wrapper begins
2. Calls next wrapper: @timer wrapper begins
3. Core Target Function: calculate_total() executes
↺ Return values propagate back up the stack in reverse order
Section 07/Advanced Pattern

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.

Tier 1: Factory(config)

Accepts custom parameters (e.g. repeat(3)) and returns the actual decorator callable.

Tier 2: Decorator(func)

Receives the target function to decorate, applying @functools.wraps(func).

Tier 3: Wrapper(*args)

Intercepts calls at runtime, using both config and func from outer closure scopes.

Python
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())
Section 08/Interactive Sandbox

Configurable Decorator Factory Sandbox

Modify factory arguments dynamically to observe how configuration propagates through closures into output logs.

Generated Decorator Invocation:
@audit_log(tag="AUDIT", times=3)
def process_event(event_id): ...
[AUDIT] Run #1: Event #9921 recorded.
[AUDIT] Run #2: Event #9921 recorded.
[AUDIT] Run #3: Event #9921 recorded.
Section 09/Stateful Wrapping

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.

Python
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)
Section 10/Callable Protocol

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__.

Python
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))
Section 11/Graduated Examples

Tiered Code Walkthroughs

Level 1 · Foundation

Basic Closure and Wrapper Anatomy

Difficulty: Beginner

The wrapper intercepts execution, prints telemetry markers before and after calling the original function, and returns the computed result back to the caller.

The wrapper intercepts execution, prints telemetry markers before and after calling the original function, and returns the computed result back to the caller. Run cleanly to earn +1 XP
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)
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.
Level 2 · Applied

Generic Wrapper with functools.wraps

Difficulty: Intermediate

@functools.wraps copies __name__, __doc__, and attaches __wrapped__, ensuring reflection, IDE autocomplete, and debugging tools see the true target function.

@functools.wraps copies __name__, __doc__, and attaches __wrapped__, ensuring reflection, IDE autocomplete, and debugging tools see the true target function. Run cleanly to earn +1 XP
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__'))
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.
Level 3 · Architecture

Inspecting Callables and Validating Types

Difficulty: Advanced

callable() checks whether an object can be called with parentheses, making it essential for defensive framework and decorator programming.

callable() checks whether an object can be called with parentheses, making it essential for defensive framework and decorator programming. Run cleanly to earn +1 XP
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))
Run cleanly to earn +1 XP: Select Run (or edit code first to experiment)! Runs that end in an error earn no XP.
Section 12/Defensive Architecture

Common Traps & Decorator Anti-Patterns

Common Trap

Syntax or Name Typos in Decorators & Function Wrappers

Buggy Pattern
# Mistyped variable or missing closing parenthesis
result = decorators___function_wrappers_val
Why it fails: Beginners frequently encounter typos or forgotten definitions before using an identifier.
Correct Production Fix
# Declare and verify variables before execution
val = 10
print("Defined correctly:", val)
Expected: NameError: name is not defined
Subtle Trap

Unintended State Mutation or Scope Leakage

Buggy Pattern
# Modifying shared state or unexpected variable shadowing
def update_state(data=[]):
    data.append(1)
    return data
Why it fails: Default arguments in Python are evaluated once at function definition time, not at invocation time.
Correct Production Fix
def update_state(data=None):
    if data is None:
        data = []
    data.append(1)
    return data
Expected: Default mutable argument accumulates values across separate function calls.
Architecture Trap

Resource Leaks & High-Scale Performance Regressions

Buggy Pattern
# Opening resources in a tight loop without closing or bounding memory
files = [open(f"log_{i}.txt", "w") for i in range(5000)]
Why it fails: Failing to release system resources leads to process termination under heavy production workloads.
Correct Production Fix
for i in range(5):
    with open(f"log_{i}.txt", "w") as f:
        f.write("Safe")
Expected: OSError: [Errno 24] Too many open files (file descriptor exhaustion)
Section 13/Industry Implementations

Real-World Architecture & Scale

Developer Productivity

🟢 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()
Cloud Microservices

🟡 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}
Distributed Systems

🔴 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 processed
Section 14/Knowledge Verification

Comprehensive Self-Check Quiz Booster

Test your understanding across all 16 conceptual, syntax, and output-prediction questions.

Question 1 of 16 • concept (easy)
What is a Python decorator at its core?
Question 2 of 16 • predict-output (easy)
What does this print?
def add_star(func):
    def wrapper():
        return "*" + func() + "*"
    return wrapper

@add_star
def hello():
    return "hi"

print(hello())
Question 3 of 16 • concept (easy)
Which symbol is used above a function definition to apply a decorator?
Question 4 of 16 • predict-output (easy)
What does this print?
def double_res(f):
    return lambda x: f(x) * 2

@double_res
def square(n):
    return n * n

print(square(3))
Question 5 of 16 • concept (medium)
Why is @functools.wraps used inside a custom decorator wrapper?
Question 6 of 16 • predict-output (medium)
What does this print?
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))
Question 7 of 16 • spot-the-bug (medium)
Which decorator implementation contains a bug by failing to return the wrapper function?
Question 8 of 16 • predict-output (medium)
What does this print?
def shout(fn):
    def wrap(msg):
        return fn(msg).upper()
    return wrap

@shout
def greet(name):
    return f"hello {name}"

print(greet("ada"))
Question 9 of 16 • syntax (hard)
Which decorator from functools caches function results to optimize expensive recursive calculations?
Question 10 of 16 • predict-output (hard)
What does this print?
def tag(fn):
    def wrap():
        return f"{fn()}"
    return wrap

@tag
def text():
    return "test"

print(text())
Question 11 of 16 • concept (hard)
When stacking @dec1 above @dec2 on def foo(): ..., in what order are the wrappers applied at definition time?
Question 12 of 16 • concept (hard)
How many levels of nested functions are needed to build a decorator that accepts custom configuration arguments (decorator factory)?
Question 13 of 16 • concept (medium)
What attribute is attached by @functools.wraps to allow programmatic access to the original unwrapped function?
Question 14 of 16 • concept (hard)
How can a Python class be used as a function decorator?
Question 15 of 16 • concept (easy)
What does the built-in function callable(obj) return?
Question 16 of 16 • spot-the-bug (medium)
What bug occurs if an inner wrapper function calls func(*args, **kwargs) but forgets to return the result?
Section 15/Hands-On Practice

Guided Practice Challenge

Official Lesson ChallengeTarget Output: ADMIN

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().

Write your uppercase_result decorator in the interactive editor.
import functools

# Define uppercase_result decorator
# Apply to get_role() and print get_role()
Section 16/Complexity Engineering

Decorator Overhead & Stack Depth

Call Frame Cost
+1 Frame / Dec

Each decorator adds exactly one Python call frame to the call stack during execution.

Definition Overhead
O(1) Once

The wrapper function is instantiated only once when the module loads, not on every function call.

LRU Cache Optimization
O(1) Lookup

@functools.lru_cache turns exponential recursive algorithms into instant hash lookups.

Section 17/Summary & Forward Path

Key Takeaways & Architectural Cheatsheet

A decorator wraps a callable, returning a new function with extended behavior (func = dec(func)).
Always decorate inner wrappers with @functools.wraps(func) to preserve __name__, __doc__, and attach __wrapped__.
Wrappers must accept (*args, **kwargs) and return the output of func(*args, **kwargs) to preserve signatures and return values.
Stacked decorators evaluate bottom-up at definition time and execute top-down at runtime.
Parameterized decorators require a three-level closure: factory(args) -> decorator(func) -> wrapper(*args, **kwargs).
Classes implementing the __call__ dunder method can serve as stateful decorators.
The built-in callable(obj) tests whether any Python object can be invoked.
Up Next: Module 06 Capstone

Lesson 05: Generators, Iterators & Yield Expressions

Master the Python iterator protocol (__iter__, __next__), infinite streams, and memory-efficient streaming pipelines.

Continue to Capstone

Worked example

Log function execution with a reusable decorator.

Example
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

  1. @log intercepts the call to ping().

Your notes

Sign in to keep private notes alongside this lesson.

Sign in to take notes

Decorators & Function Wrappers practice

Sign-in required

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

Locked

You must be signed in to take this quiz. Sign in to test your understanding, track your answers, and earn XP.

Lesson incomplete

Not completed

Create an ID or sign in to save lesson completion across devices.

Create ID to save
Module lesson progress0/5 complete
Module cheat sheet