Skip to content

E.B.1 Advanced Decorator Usage

Python decorator execution flow

Decorator cross-cutting logic layering diagram

A decorator wraps a function with reusable outer behavior. Use it when many functions need the same logic, such as logging, timing, retry, permission checks, or tracing.

  • Python 3.10+
  • No external packages
  • Basic understanding of functions
  • Wrapper: the inner function that runs before and after the original function.
  • Cross-cutting logic: logic needed in many places but not part of the business task itself.
  • functools.wraps: keeps the original function name and metadata after decoration.
  • Decorator order: the top decorator runs first when the function is called.

Create decorator_demo.py:

from functools import wraps
def log_call(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
print(f"[LOG] start {fn.__name__}")
result = fn(*args, **kwargs)
print(f"[LOG] end {fn.__name__}")
return result
return wrapper
def retry(max_retries=2):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
last_error = None
for attempt in range(1, max_retries + 2):
try:
return fn(*args, **kwargs)
except RuntimeError as error:
last_error = error
print(f"[RETRY] attempt={attempt} error={error}")
raise last_error
return wrapper
return decorator
state = {"attempt": 0}
@log_call
@retry(max_retries=2)
def fetch_model_info(model_id):
state["attempt"] += 1
if state["attempt"] < 2:
raise RuntimeError("temporary network error")
return {"model_id": model_id, "status": "ready"}
print(fetch_model_info("demo-v1"))
print(fetch_model_info.__name__)

Run it:

Terminal window
python decorator_demo.py

Expected output:

Terminal window
[LOG] start fetch_model_info
[RETRY] attempt=1 error=temporary network error
[LOG] end fetch_model_info
{'model_id': 'demo-v1', 'status': 'ready'}
fetch_model_info

This shows three useful details: the business function stays short, retry behavior is centralized, and wraps preserves the function name.

When a decorated function behaves strangely, do not start by rewriting the business function. First inspect the wrapper order, the exception types being caught, and whether @wraps preserved the function name. Many framework bugs look mysterious because decorators have changed metadata or swallowed the real error.

In AI systems, decorators are most useful around unstable boundaries: API calls, tool calls, permission checks, and metrics. Keep the decorator small enough that a teammate can predict exactly what runs before and after the original function.

Swap the decorators:

@retry(max_retries=2)
@log_call
def fetch_model_info(model_id):

Now logging runs inside each retry attempt. This is why decorator order matters in service code.

Use decorators for:

  1. Logging and tracing
  2. Timing
  3. Retry around unstable I/O
  4. Permission checks
  5. Framework registration

Avoid decorators when the wrapper hides important business logic or when one function already has too many layers.

Before using a decorator in production, check whether the wrapped function still behaves like itself. The input should be unchanged, the return value should be unchanged, and the extra behavior should be visible in logs, timing output, or metrics.

A useful decorator leaves evidence without hiding control flow. If debugging becomes harder, prefer a plain helper function. The goal is not clever syntax; the goal is a repeatable way to add tracing, validation, retry, or timing around many similar functions.

Keep this page’s proof of learning as a small evidence card:

Python Pattern
decorator, iterator, generator, concurrency primitive, or metaprogramming hook
Code Artifact
minimal runnable example plus printed output
Use Case
where this pattern improves an AI app, pipeline, tool, or server
Failure Check
hidden side effects, unreadable abstraction, race condition, or overengineering
Expected Output
small advanced-Python example with a practical AI-system use note
  • Forgetting @wraps, then logs and frameworks see every function as wrapper.
  • Retrying every exception, including validation or permission errors that should fail immediately.
  • Stacking many decorators until the execution order is hard to debug.

Add a require_role("admin") decorator before fetch_model_info. Make it raise PermissionError for non-admin users, and do not retry permission errors.

Reference implementation and walkthrough

A good implementation checks authorization before the retry wrapper handles transient failures. One clean route is to place require_role("admin") outside the retrying call path, or update retry so it immediately re-raises PermissionError.

The expected behavior is:

  • Admin users call the function normally.
  • Non-admin users get PermissionError.
  • The retry log does not repeat permission failures, because permission is not a temporary network error.