Python Decorators: A Practical Guide
Master Python decorators with practical examples for logging, authentication, caching, and more.
Introduction
Decorators are one of the most powerful — and most misunderstood — features in Python.
They allow you to:
- ✔ Add functionality to existing functions without modifying their source code
- ✔ Implement logging, authentication, caching, rate-limiting, and more
- ✔ Write cleaner, more reusable, more professional code
The best part?
You can master decorators even as an intermediate Python programmer. This guide explains decorators clearly, with practical examples you can use immediately.
1. What Is a Decorator in Python?
A decorator is simply a function that takes another function and adds extra behaviour to it — without changing the original function's code.
Think of it like wrapping a gift:
- 🎁 gift = original function
- 🎁 wrapping paper = decorator
- 🎁 wrapped gift = enhanced function
Basic structure:
def decorator(func):
def wrapper():
print("Before")
func()
print("After")
return wrapperThen you apply it using:
@decorator
def greet():
print("Hello!")2. Why Use Decorators?
Decorators are used everywhere in professional Python codebases because they help solve common problems:
- ⭐ Logging — Record when a function is called.
- ⭐ Authentication — Check if a user is authorized.
- ⭐ Speed Measurement — Find performance bottlenecks.
- ⭐ Retry Logic — Retry failed API calls.
- ⭐ Validation — Ensure function arguments are correct.
- ⭐ Caching (Memoization) — Return saved results for expensive functions.
If you plan to build web apps, APIs, AI scripts, or automation — decorators will become your favourite tool.
3. Your First Useful Decorator (Logging Example)
Let's build a decorator that logs when a function is executed.
def logger(func):
def wrapper(*args, **kwargs):
print(f"Running {func.__name__}...")
result = func(*args, **kwargs)
print(f"{func.__name__} finished.")
return result
return wrapper
@logger
def say_hello():
print("Hello!")
say_hello()Output:
Running say_hello...
Hello!
say_hello finished.Just like that, you've added logging around the function — without touching its code.
4. Decorators With Arguments
Sometimes, you want the decorator itself to receive extra arguments.
Example: rate-limiting a function.
import time
def delay(seconds):
def decorator(func):
def wrapper(*args, **kwargs):
print(f"Waiting {seconds} seconds...")
time.sleep(seconds)
return func(*args, **kwargs)
return wrapper
return decorator
@delay(2)
def greet():
print("Hello!")
greet()This uses a three-level decorator, a common pattern:
- outer → receives decorator arguments
- middle → receives the function
- inner → runs the function
5. Practical Example: Measuring Function Speed
Performance monitoring is essential in ML, data pipelines, and backend work.
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"{func.__name__} ran in {end - start:.4f} seconds")
return result
return wrapper
@timer
def slow_function():
time.sleep(1)
slow_function()This decorator is used in real-world apps to identify slow functions.
6. Using functools.wraps (Important!)
Without functools.wraps, decorators break metadata:
- docstrings
- function names
- IDE/autocomplete behaviour
Correct version:
from functools import wraps
def logger(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapperAlways use @wraps in production code.
7. Real-World Use Case: Authentication Decorator
This is the type of decorator used in Flask, Django, and FastAPI.
def require_login(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if not user.get("logged_in"):
raise PermissionError("You must be logged in!")
return func(user, *args, **kwargs)
return wrapper
@require_login
def view_dashboard(user):
return "Dashboard content"
user = {"logged_in": True}
print(view_dashboard(user))If user is not logged in → error. This pattern powers authentication systems across the web.
8. Caching / Memoization Decorator (Performance Boost)
Useful for expensive computations — like ML preprocessing, API calls, Fibonacci, etc.
def cache(func):
stored = {}
@wraps(func)
def wrapper(*args):
if args in stored:
return stored[args]
result = func(*args)
stored[args] = result
return result
return wrapper
@cache
def expensive_computation(n):
print("Computing...")
return n * n
print(expensive_computation(10)) # Computes
print(expensive_computation(10)) # Cached9. Stacking Multiple Decorators
You can apply more than one:
@timer
@logger
def run():
print("Running task...")Order matters:
- Logger wraps inner function
- Timer wraps logger
10. Summary
By now, you should understand:
- ✔ What decorators are
- ✔ Why they're useful
- ✔ How they wrap existing functions
- ✔ How to add arguments to decorators
- ✔ How to log, time, validate, authenticate
- ✔ How to use functools.wraps
- ✔ How real frameworks rely heavily on decorators
Decorators are essential in:
- Flask / Django / FastAPI routing
- ML model caching
- Logging systems
- Database middleware
- API request validation
- Authentication layers
Mastering decorators is a major step toward becoming a professional Python developer.