Python Decorators: The Next-Level Guide (Beginner → Pro)
Ever copy-pasted the same “logging/timing/auth check” code into a dozen functions? I did too. It felt like taping gadgets onto every tool in my toolbox — messy and hard to maintain.
Decorators are the tidy, grown-up way to add that extra behavior without touching every function’s core logic. Think of them like putting a helpful “attachment” on your function — the function still works, but now it gets superpowers.
What you’ll learn (in plain English)
- What decorators are (and why they’re awesome).
- How to write your own (simple → advanced).
functools.wraps(the one thing most beginners forget).- Parameterized decorators, stacked decorators, and method decoration.
- Practical patterns: logging, timing, caching, retrying, access control.
- Class-based decorators, decorating classes, and async-aware decorators.
- Common pitfalls and a quick cheat-sheet.
1) What is a decorator, really?
A decorator is a function that takes another function and returns a new function.
It’s like putting a jacket on your function: same person, but now warmer and with pockets. 🧥
Without the @ sugar:
def shout(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return str(result).upper()
return wrapper
def greet(name):
return f"Hello, {name}"
greet = shout(greet) # manually decorate
print(greet("Ada")) # HELLO, ADA
With the @ sugar (preferred):
@shout
def greet(name):
return f"Hello, {name}"
Takeaway: A decorator wraps a function to add behavior before/after/around the original call.
2) First decorator (the right way) — use functools.wraps
This preserves the original function’s name, docstring, and annotations.
Without it, debugging and docs become confusing.
from functools import wraps
def shout(func):
@wraps(func)
def wrapper(*args, **kwargs):
value = func(*args, **kwargs)
return str(value).upper()
return wrapper
Pro Tip: Always use @wraps. Trust me, you’ll thank yourself later.
3) Decorator with arguments (aka a decorator factory)
Sometimes you want to configure the decorator (e.g., log level, retries).
from functools import wraps
def repeat(times=2):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
last = None
for _ in range(times):
last = func(*args, **kwargs)
return last
return wrapper
return decorator
@repeat(times=3)
def ping():
print("pong")
ping() # prints "pong" three times
How it works: repeat(3) runs first and returns the actual decorator.
4) Stacking multiple decorators (order matters!)
Decorators closest to the function run first at call time, but are applied last at definition time.
def A(f):
@wraps(f)
def w(*a, **k):
print("A before")
out = f(*a, **k)
print("A after")
return out
return w
def B(f):
@wraps(f)
def w(*a, **k):
print("B before")
out = f(*a, **k)
print("B after")
return out
return w
@A
@B
def work():
print("doing work")
work()
# A before
# B before
# doing work
# B after
# A after
Warning: Changing the order changes behavior. Be intentional.
5) Decorating methods (don’t forget self)
Methods are just functions that receive self as the first argument. Your wrapper should accept *args, **kwargs, and it’ll work fine.
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__qualname__}")
return func(*args, **kwargs)
return wrapper
class User:
@log_call
def save(self):
print("saved!")
User().save()
# Calling User.save
# saved!
Pro Tip: Works the same for @classmethod and @staticmethod.
6) Practical, real-world decorators
a) Logging
from functools import wraps
import logging
logging.basicConfig(level=logging.INFO)
def log_io(func):
@wraps(func)
def wrapper(*args, **kwargs):
logging.info("→ %s args=%s kwargs=%s", func.__name__, args, kwargs)
out = func(*args, **kwargs)
logging.info("← %s returns %r", func.__name__, out)
return out
return wrapper
b) Timing
import time
from functools import wraps
def timing(func):
@wraps(func)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
dt = (time.perf_counter() - t0) * 1000
print(f"{func.__name__} took {dt:.2f} ms")
return wrapper
c) Caching (built-in!)
from functools import lru_cache
@lru_cache(maxsize=1024)
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
d) Retry with backoff
import time, random
from functools import wraps
def retry(times=3, delay=0.2, factor=2.0):
def decorator(func):
@wraps(func)
def wrapper(*a, **k):
wait = delay
for attempt in range(1, times + 1):
try:
return func(*a, **k)
except Exception as e:
if attempt == times:
raise
time.sleep(wait)
wait *= factor
return wrapper
return decorator
@retry(times=3, delay=0.1)
def flaky():
if random.random() < 0.7:
raise RuntimeError("try again")
return "ok"
e) Access control (simple role check)
from functools import wraps
def requires_role(role):
def decorator(func):
@wraps(func)
def wrapper(user, *args, **kwargs):
if role not in getattr(user, "roles", []):
raise PermissionError("forbidden")
return func(user, *args, **kwargs)
return wrapper
return decorator
7) Class-based decorators (stateful wrappers)
When you need to store state (e.g., count calls), a class with __call__ is handy.
from functools import update_wrapper
class count_calls:
def __init__(self, func):
self.func = func
self.count = 0
update_wrapper(self, func) # like @wraps but for classes
def __call__(self, *args, **kwargs):
self.count += 1
print(f"{self.func.__name__} call #{self.count}")
return self.func(*args, **kwargs)
@count_calls
def hello():
print("hi")
hello()
hello()
# hello call #1
# hi
# hello call #2
# hi
8) Decorating classes (not just functions!)
A class decorator receives a class and returns a class (or a modified one).
def add_repr(cls):
def __repr__(self):
fields = ", ".join(f"{k}={v!r}" for k, v in self.__dict__.items())
return f"{cls.__name__}({fields})"
cls.__repr__ = __repr__
return cls
@add_repr
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
print(Point(2, 3))
# Point(x=2, y=3)
Use cases: auto-register plugins, validate class attributes, attach mixins.
9) Async-aware decorators (works for async def)
If you decorate async functions, your decorator should detect and await them.
import asyncio, inspect
from functools import wraps
def async_aware(deco):
"""Wrap a decorator so it supports sync + async functions."""
def outer(func):
if inspect.iscoroutinefunction(func):
@wraps(func)
async def async_wrapper(*a, **k):
return await deco(func)(*a, **k)
return async_wrapper
else:
@wraps(func)
def sync_wrapper(*a, **k):
return deco(func)(*a, **k)
return sync_wrapper
return outer
@async_aware
def log_async(func):
@wraps(func)
async def wrapper(*a, **k):
print("before")
try:
return await func(*a, **k)
finally:
print("after")
return wrapper
@log_async
async def fetch():
await asyncio.sleep(0.01)
return 42
10) Common pitfalls (and how to avoid them)
- Forgetting
@wraps→ you’ll lose__name__,__doc__, and type hints. - Changing function signatures → keep
*args, **kwargsunless you really need otherwise. - Decorator order surprises → remember: top decorator runs outermost.
- Hiding exceptions → log and re-raise; don’t swallow errors silently.
- Global state in decorators → can cause test flakiness. Prefer arguments or class-based state.
11) Mini cheat-sheet
- Simple decorator: takes
func→ returnswrapper. - With args: takes
*config→ returnsdecorator→ returnswrapper. - Always: use
@wraps(func)inside the wrapper. - Stacking:
@Aabove@B⇒ A wraps B wraps func. - Async: detect with
inspect.iscoroutinefunctionandawait. - Stateful: use class with
__call__+update_wrapper.
12) Put it all together: a tidy, production-ready pattern
import logging, time, inspect
from functools import wraps, update_wrapper
logging.basicConfig(level=logging.INFO)
def compose(*decorators):
"""Compose multiple decorators as one."""
def combined(func):
for d in reversed(decorators):
func = d(func)
return func
return combined
def timeit(label=None):
def deco(func):
@wraps(func)
def wrapper(*a, **k):
t0 = time.perf_counter()
try:
return func(*a, **k)
finally:
dt = (time.perf_counter() - t0) * 1000
logging.info("%s took %.2f ms", label or func.__name__, dt)
return wrapper
return deco
def log_call(func):
@wraps(func)
def wrapper(*a, **k):
logging.info("→ %s", func.__qualname__)
out = func(*a, **k)
logging.info("← %s", func.__qualname__)
return out
return wrapper
@compose(timeit("sum_up"), log_call)
def add_up(n):
return sum(range(n))
print(add_up(1_000_000))
Tiny mental model (ASCII diagram)
@A
@B
def f(): pass
call → A.wrapper( B.wrapper( f ) )
Remember: A is the outer jacket, B is the inner one.
FAQ (Beginner-friendly)
1) When should I use a decorator vs. a helper function?
Use a decorator when you want the same cross-cutting behavior applied uniformly (logging, timing, auth). Use a helper when the logic is domain-specific to a single function.
2) Why does my decorated function lose its name/docs?
Because your wrapper replaced it. Add @functools.wraps(original_func) on the wrapper to preserve metadata.
3) Can I pass arguments to decorators?
Yes! That’s a decorator factory: @retry(times=3). It’s just one extra function layer returning the actual decorator.
Wrap-up (and your next step)
Decorators help you add behavior without clutter, keep code DRY, and make cross-cutting concerns feel effortless. Start small: add a @timing decorator to one hot path, then try a @retry for flaky calls, and graduate to async-aware or class-based patterns as you go.
Your turn: What’s the first function in your codebase you’d love to decorate — logging, timing, or retrying? Share your pick and why in the comments!
Projects to explore next
Source-code projects from the KritiMyantra catalogue. Check each project's details before buying.
Flask Authentication System (Free Version) - Kritim Yantra
🛠 Features✔ Google & GitHub Authentication✔ User Registration & Login✔ Profile/Dashboard Access✔ Secure & Mi...
Flask Blog CMS with Admin Dashboard — Python Source Code
A Flask-based blogging website with a built-in admin dashboard. Manage articles, images, categories, tags and featured p...
Laravel 12 + ReactJS Starter Kit: Add Multi-Language Support (Arabic, English, Spanish) with Blog CRUD
🚀 Installation & Setup Download & Extract Get the ZIP from kritimyantra.com and extract it. Includes preconfigu...
Comments
No comments yet. Be the first to comment.
Sign in to join the discussion.