Python Decorators — Core Concepts
Decorators are one of Python’s most powerful patterns for cross-cutting behavior: things like logging, authentication, timing, retries, validation, and caching.
Instead of repeating that logic inside every function, you place it in one decorator and apply it where needed.
What a Decorator Really Does
This line:
@my_decorator
def get_user():
...
is equivalent to:
def get_user():
...
get_user = my_decorator(get_user)
my_decorator receives the original function and returns a new function (often called wrapper). Calls to get_user now go through that wrapper.
Basic Structure
def log_calls(func):
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
result = func(*args, **kwargs)
print(f"Finished {func.__name__}")
return result
return wrapper
*args and **kwargs make the decorator work with almost any function signature.
Common Use Cases
1) Authorization
def require_admin(func):
def wrapper(user, *args, **kwargs):
if not user.get("is_admin"):
raise PermissionError("Admin required")
return func(user, *args, **kwargs)
return wrapper
2) Timing
import time
def timeit(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
3) Caching
For expensive pure functions, decorators can memoize results. Standard library version: functools.lru_cache.
from functools import lru_cache
@lru_cache(maxsize=256)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)
Preserving Function Metadata
Without help, wrappers replace function identity (__name__, docstring, signature). Use functools.wraps:
from functools import wraps
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
This matters for debugging, documentation tools, and frameworks that inspect function signatures.
Decorators With Arguments
Sometimes you need options, like retry count.
from functools import wraps
def retry(times=3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last_error = None
for _ in range(times):
try:
return func(*args, **kwargs)
except Exception as e:
last_error = e
raise last_error
return wrapper
return decorator
@retry(times=5)
def fetch_data():
...
Pattern:
- outer function gets config (
times=5) - middle function gets
func - inner function wraps execution
Common Misconception
Misconception: decorators are only for advanced framework magic.
Reality: decorators are practical in everyday scripts and apps whenever you see repeated “before/after function call” logic.
If you copy-paste the same pre-check or post-log into multiple functions, a decorator is often the cleanest refactor.
One Thing to Remember
A decorator is function composition with readable syntax: write reusable wrapper behavior once, then attach it to many functions with
@....
See Also
- Python Async Await Async/await helps one Python program juggle many waiting jobs at once, like a chef who keeps multiple pots moving without standing still.
- Python Basics Python is the programming language that reads like plain English — here's why millions of beginners (and experts) choose it first.
- Python Booleans Make Booleans click with one clear analogy you can reuse whenever Python feels confusing.
- Python Break Continue Make Break Continue click with one clear analogy you can reuse whenever Python feels confusing.
- Python Closures See how Python functions can remember private information, even after the outer function has already finished.