Lesson 14 • Expert
Decorators & Advanced Features
Decorators are one of Python's most powerful features — used in Django, Flask, FastAPI, TensorFlow, PyTorch, logging systems, authentication, caching, and more. This lesson takes you far beyond the basics.
What You'll Learn in This Lesson
- • What a decorator is and how Python applies
@syntax - • Writing your own decorators from scratch with
functools.wraps - • Stacking multiple decorators and decorator factories with arguments
- • Practical use cases: timing, logging, access control, caching
- • How real frameworks (Django, FastAPI) use advanced decorator patterns
What You'll Learn
✔ How decorators truly work under the hood
✔ How to pass arguments to decorators
✔ How to stack multiple decorators safely
✔ How to preserve metadata (the functools.wraps problem)
✔ How real frameworks use advanced decorator patterns
✔ How to build your own production-ready decorator utilities
🔥 1. Decorators Refresher (1-Minute Summary)
🏠 Real-World Analogy:
Think of a decorator like gift wrapping. You have a present (your function), and the wrapper adds something extra (like a bow or ribbon) without changing what's inside. The wrapper can add behavior before or after opening the gift!
| Step | What Happens |
|---|---|
| 1. Define decorator | Create a function that takes another function as input |
| 2. Create wrapper | Inside, define a wrapper function that adds behavior |
| 3. Return wrapper | Return the wrapper (not call it!) |
| 4. Apply with @ | Use @decorator_name above a function |
The minimal example:
Basic Decorator
The simplest decorator pattern
# Step 1: Define the decorator function
def my_decorator(fn): # Takes a function as input
# Step 2: Create an inner "wrapper" function
def wrapper():
print("Before") # Runs BEFORE the original function
fn() # Call the original function
print("After") # Runs AFTER the original function
# Step 3: Return the wrapper (don't call it!)
return wrapper
# Step 4: Apply decorator using @ syntax
@my_decorator
def hello():
print("Hello!"
...@my_decorator is just shorthand for hello = my_decorator(hello). The decorator replaces your function with the wrapper!Try It Yourself: Decorators
Practice creating and using decorators in Python
# Decorators Practice
import time
from functools import wraps
# Basic decorator
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before the function")
result = func(*args, **kwargs)
print("After the function")
return result
return wrapper
@my_decorator
def say_hello(name):
print(f"Hello, {name}!")
say_hello("Alice")
print("\n" + "="*30 + "\n")
# Timer decorator
def timer(func):
@wraps(func)
def wrapper(*args,
...🧠 2. Why Decorators Matter in Real Projects
Every major Python ecosystem uses decorators for:
Authentication Decorator
Protect routes with login check
# Authentication decorator example
def require_login(fn):
def wrapper(user, *args, **kwargs):
if not user.get("logged_in"):
return "Please log in first!"
return fn(user, *args, **kwargs)
return wrapper
@require_login
def get_dashboard(user):
return f"Welcome to your dashboard, {user['name']}!"
# Test with logged in user
logged_in_user = {"name": "Alice", "logged_in": True}
print(get_dashboard(logged_in_user))
# Test with logged out user
logged_out_user
...✔ Logging & monitoring
✔ Rate limiting
✔ Permissions
✔ Dependency injection (FastAPI)
✔ ML model preprocessing
Understanding decorators = understanding real frameworks.
⚙️ 3. The Core Issue — Decorators Remove Metadata
⚠️ The Problem:
When you wrap a function, Python "forgets" the original function's name, docstring, and other info. This breaks documentation tools, debugging, and frameworks like FastAPI!
| Without @wraps | With @wraps ✅ |
|---|---|
func.__name__ → "wrapper" | func.__name__ → original name |
func.__doc__ → None | func.__doc__ → original docstring |
| Debugger shows "wrapper" | Debugger shows real function name |
functools.wraps
Preserve function metadata
from functools import wraps
# ❌ BAD: Without wraps - loses metadata
def bad_decorator(fn):
def wrapper(*a, **k):
return fn(*a, **k)
return wrapper # Returns wrapper, not fn!
@bad_decorator
def greet():
"""Says hello"""
return "Hello!"
print("Without @wraps:")
print(f" Name: {greet.__name__}") # 'wrapper' - WRONG!
print(f" Doc: {greet.__doc__}") # None - WRONG!
# ✅ GOOD: With wraps - preserves metadata
def good_decorator(fn):
@wraps(fn) # ← This line fixes
...@wraps(fn) in your decorators. It's mandatory for production code!🎯 4. Decorators That Accept Arguments (Decorator Factories)
🤔 The Challenge:
What if you want @check_role("admin")? You need to pass an argument to the decorator! This requires an extra layer of nesting called a decorator factory.
| Layer | Purpose | Returns |
|---|---|---|
| Outer function | Accepts decorator arguments | Returns the actual decorator |
| Middle function | The actual decorator (takes fn) | Returns the wrapper |
| Inner function | The wrapper that runs | Calls the original fn |
Decorator Factory
Decorators with arguments
from functools import wraps
# Layer 1: Outer function accepts the argument
def check_role(required_role):
# Layer 2: This is the actual decorator
def decorator(fn):
@wraps(fn)
# Layer 3: This is the wrapper that runs
def wrapper(user, *args, **kwargs):
if user.get("role") != required_role:
raise PermissionError(f"Access denied. Required: {required_role}")
return fn(user, *args, **kwargs)
return wrapper
return de
...defs! Simple decorator = 2 defs. Decorator with arguments = 3 defs.🔄 5. Stacking Multiple Decorators (Order Matters!)
🏠 Analogy: Layers of Wrapping Paper
Imagine wrapping a gift with multiple layers. The closest decorator to the function wraps first, then the next one wraps around that, and so on. When you call the function, you "unwrap" from outside in.
| Code Order | Execution Order |
|---|---|
| @A (top) | A runs FIRST (outermost layer) |
| @B (bottom) | B runs SECOND (inner layer) |
| def func: | Function runs LAST (the core) |
Stacking Decorators
Order matters in decorator chains
from functools import wraps
def decorator_A(fn):
@wraps(fn)
def wrapper(*a, **k):
print("A: before") # Runs 1st
result = fn(*a, **k) # Calls B's wrapper
print("A: after") # Runs 6th
return result
return wrapper
def decorator_B(fn):
@wraps(fn)
def wrapper(*a, **k):
print("B: before") # Runs 2nd
result = fn(*a, **k) # Calls the real function
print("B: after") # Runs 4th
return result
return wrapper
...⚡ 6. Example: Timing + Logging + Caching Stack
Full Decorator Stack
Timer, logger, and cache combined
import time
from functools import wraps
def timer(fn):
@wraps(fn)
def wrapper(*a, **k):
start = time.time()
result = fn(*a, **k)
print(f"⏱ {fn.__name__} took {time.time() - start:.4f}s")
return result
return wrapper
def logger(fn):
@wraps(fn)
def wrapper(*a, **k):
print(f"📘 Calling {fn.__name__}")
return fn(*a, **k)
return wrapper
def cache(fn):
saved = {}
@wraps(fn)
def wrapper(x):
if x in saved:
...🧩 7. Real Project Example — Retry Decorator
Used in API calls, database queries, and cloud services.
Retry Decorator
Auto-retry failed operations
from functools import wraps
import time
import random
def retry(times):
def decorator(fn):
@wraps(fn)
def wrapper(*args, **kwargs):
for i in range(times):
try:
return fn(*args, **kwargs)
except Exception as e:
print(f"Retry {i+1}/{times}: {e}")
time.sleep(0.1)
raise RuntimeError("All retries failed")
return wrapper
return decorator
@retry(3)
def un
...🔒 8. Real Project Example — Input Validation Decorator
Input Validation
Type-check function arguments
from functools import wraps
def validate_types(*types):
def decorator(fn):
@wraps(fn)
def wrapper(*args):
for arg, expected in zip(args, types):
if not isinstance(arg, expected):
raise TypeError(f"Expected {expected.__name__}, got {type(arg).__name__}")
return fn(*args)
return wrapper
return decorator
@validate_types(int, int)
def add(a, b):
return a + b
print(add(5, 3))
try:
print(add("5", 3)
...🧠 9. Passing Multiple Arguments to Decorators
Multiple Arguments
Decorators with multiple parameters
from functools import wraps
def require_roles(*allowed):
def decorator(fn):
@wraps(fn)
def wrapper(user, *a, **k):
if user.get("role") not in allowed:
return f"Access denied. Allowed roles: {allowed}"
return fn(user, *a, **k)
return wrapper
return decorator
@require_roles("admin", "moderator")
def edit_post(user, post_id):
return f"{user['name']} edited post {post_id}"
admin = {"name": "Alice", "role": "admin"}
mod =
...🧵 10. Decorators With Keyword Arguments
Keyword Arguments
Configurable decorators
from functools import wraps
def log(prefix="INFO", suffix=""):
def decorator(fn):
@wraps(fn)
def wrapper(*a, **k):
print(f"[{prefix}] Calling {fn.__name__} {suffix}")
return fn(*a, **k)
return wrapper
return decorator
@log(prefix="DEBUG", suffix="- testing")
def load_config():
return {"debug": True}
@log(prefix="WARN")
def risky_operation():
return "done"
load_config()
risky_operation()🎓 11. Advanced Pattern — Class-Based Decorators
🤔 When to Use Classes?
Use class-based decorators when you need to track state across multiple calls (like counting calls, caching results, or enforcing rate limits).
| Function Decorator | Class Decorator |
|---|---|
| Simple, one-off behavior | Needs state between calls |
| Uses closures for state | Uses self attributes |
| 3 nested functions max | Cleaner for complex logic |
Class-Based Decorator
Stateful decorators with classes
from functools import wraps
class RateLimiter:
def __init__(self, limit):
self.limit = limit # Max allowed calls
self.calls = 0 # State: track call count
def __call__(self, fn): # Makes the class callable as decorator
@wraps(fn)
def wrapper(*a, **k):
if self.calls >= self.limit:
return f"Rate limit exceeded ({self.limit} calls max)"
self.calls += 1 # Update state
return fn(*a, **k)
...__init__ stores the arguments,__call__ makes the instance work like a function (the actual decorator).🧨 12. Debugging Decorators (Common Problems)
These are the most common mistakes when writing decorators:
| ❌ Mistake | What Happens | ✅ Fix |
|---|---|---|
Forgetting @wraps | Function name/docs disappear | Always add @wraps(fn) |
| Wrong decorator order | Unexpected behavior | Remember: bottom wraps first |
Missing *args, **kwargs | Arguments don't pass through | Always use wrapper(*a, **k) |
| Forgetting to return result | Function returns None | Add return fn(*a, **k) |
| Calling instead of returning wrapper | Decorator runs immediately | Use return wrapper not return wrapper() |
❌ Forgetting wraps
→ Breaks documentation, tooling, introspection, FastAPI routes, pytest
❌ Using wrong order in stacks
→ Decorators run in unexpected order; auth might run after logging
❌ Forgetting closure rules
→ Modifying outer variable without nonlocal causes errors
🧪 13. Mini Project — Build a Full Decorator Suite
Build decorators for timing, logging, validation, caching, retries, and permissions:
Full Decorator Suite
Framework-level decorator system
from functools import wraps
import time
def logger(fn):
@wraps(fn)
def wrapper(*a, **k):
print(f"📘 {fn.__name__} called")
return fn(*a, **k)
return wrapper
def timer(fn):
@wraps(fn)
def wrapper(*a, **k):
start = time.time()
result = fn(*a, **k)
print(f"⏱ {time.time() - start:.4f}s")
return result
return wrapper
def retry(times):
def decorator(fn):
@wraps(fn)
def wrapper(*a, **k):
for i in
...You now have a framework-level decorator system.
🎉 Conclusion
You now understand:
✔ How decorators truly work
✔ How to create decorators that accept arguments
✔ How to stack and combine decorators
✔ How to preserve metadata
✔ How closures power all decorators
✔ How major Python frameworks implement them
✔ How to design your own production-ready decorator system
📋 Quick Reference — Decorators
| Syntax | What it does |
|---|---|
| @my_decorator | Apply decorator to a function |
| functools.wraps(fn) | Preserve original function metadata |
| @staticmethod | Method that needs no self |
| @classmethod | Method that receives the class |
| @lru_cache | Cache results for repeated calls |
🏆 Lesson Complete!
You can now write, stack, and configure decorators for any use case — the same pattern used by Flask, Django, and FastAPI.
Up next: Advanced Functions — master *args, **kwargs, and function signature tools.
Sign up for free to track which lessons you've completed and get learning reminders.