Decorators
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.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
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 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
🧠 2. Why Decorators Matter in Real Projects
Every major Python ecosystem uses decorators for:
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
🎯 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
🔄 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)
⚡ 6. Example: Timing + Logging + Caching Stack
🧩 7. Real Project Example — Retry Decorator
Used in API calls, database queries, and cloud services.
🔒 8. Real Project Example — Input Validation Decorator
🧠 9. Passing Multiple Arguments to Decorators
🧵 10. Decorators With Keyword Arguments
🎓 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
🧨 12. Debugging Decorators (Common Problems)
These are the most common mistakes when writing decorators:
❌ Mistake
✅ 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()
→ Breaks documentation, tooling, introspection, FastAPI routes, pytest
→ Decorators run in unexpected order; auth might run after logging
→ 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:
You now have a framework-level decorator system.
🎉 Conclusion
✔ How to create decorators that accept arguments
✔ How to design your own production-ready decorator system
📋 Quick Reference — Decorators
Syntax
What it does
🏆 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.
Practice quiz
What does the @my_decorator syntax above a function actually do?
- Calls the function immediately
- Imports a module
- Is shorthand for func = my_decorator(func)
- Defines a class
Answer: Is shorthand for func = my_decorator(func). @my_decorator is shorthand for func = my_decorator(func) — it replaces func with the wrapper.
In a basic decorator, what should you do with the inner wrapper function?
- Return it (not call it): return wrapper
- Call it: return wrapper()
- Delete it
- Print it
Answer: Return it (not call it): return wrapper. You return the wrapper itself (return wrapper); calling it would run it immediately.
What does functools.wraps(fn) preserve?
- The function's speed
- The return value
- The arguments
- The original function's __name__ and __doc__ (its metadata)
Answer: The original function's __name__ and __doc__ (its metadata). @wraps copies metadata like __name__ and __doc__ from the original onto the wrapper.
Without @wraps, what does the decorated function's __name__ become?
- The original name
- 'wrapper'
- None
- 'decorator'
Answer: 'wrapper'. Without @wraps, the metadata is lost and __name__ shows 'wrapper'.
How many def statements does a decorator that accepts arguments (a decorator factory) typically have?
- 3
- 1
- 2
- 4
Answer: 3. A decorator with arguments has 3 levels: outer (takes args), middle (takes fn), inner wrapper.
With @decorator_A on top of @decorator_B, which wraps the function first?
- A (the top one)
- They wrap simultaneously
- B (the bottom one, closest to the function)
- Neither
Answer: B (the bottom one, closest to the function). The bottom decorator (closest to the function) wraps first; the top one runs first when called.
Why use *args, **kwargs in the wrapper signature?
- To make it faster
- So arguments pass through to the wrapped function correctly
- To preserve metadata
- It is required syntax for all functions
Answer: So arguments pass through to the wrapped function correctly. wrapper(*args, **kwargs) lets the wrapper accept and forward any arguments to fn.
For a class-based decorator, which method makes the instance usable as a decorator?
- __init__
- __main__
- __wrap__
- __call__
Answer: __call__. __call__ makes the instance callable, so it acts as the decorator; __init__ stores its arguments.
When is a class-based decorator most useful?
- For simple one-off behavior
- When you need to track state across multiple calls
- When you want fewer lines
- When the function has no arguments
Answer: When you need to track state across multiple calls. Class decorators use self attributes to track state (like call counts or rate limits) between calls.
In the validate_types decorator, what does add('5', 3) raise?
- ValueError
- Nothing, it returns '53'
- TypeError
- KeyError
Answer: TypeError. '5' is a str, not an int, so the isinstance check fails and raises TypeError.