Skip to main 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.

What You Need

  • Python 3.10+
  • No external packages
  • Basic understanding of functions

Key Terms

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

Run A Logging And Retry Decorator

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:

python decorator_demo.py

Expected output:

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

Change The Order

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.

When To Use Decorators

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.

Common Mistakes

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

Practice

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