Kritim Yantra
Aug 15, 2025
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.
functools.wraps
(the one thing most beginners forget).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.
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.
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.
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.
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
.
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
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
from functools import lru_cache
@lru_cache(maxsize=1024)
def fib(n):
if n < 2: return n
return fib(n-1) + fib(n-2)
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"
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
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
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.
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
@wraps
→ you’ll lose __name__
, __doc__
, and type hints.*args, **kwargs
unless you really need otherwise.func
→ returns wrapper
.*config
→ returns decorator
→ returns wrapper
.@wraps(func)
inside the wrapper.@A
above @B
⇒ A wraps B wraps func.inspect.iscoroutinefunction
and await
.__call__
+ update_wrapper
.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))
@A
@B
def f(): pass
call → A.wrapper( B.wrapper( f ) )
Remember: A is the outer jacket, B is the inner one.
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.
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!
No comments yet. Be the first to comment!
Please log in to post a comment:
Sign in with Google