Decorators, End to End
Wrap functions to add behaviour — plain decorators, decorators that take arguments, and class-based ones.
A decorator is a function that takes a function and returns a new function with extra behaviour wrapped around it. The @decorator syntax is just sugar: @log above def f means f = log(f).
The basic shape
import functools
def shout(func):
@functools.wraps(func) # keep func's name and docstring
def wrapper(*args, **kwargs):
return func(*args, **kwargs).upper()
return wrapper
@shout
def greet(name):
return f'hi {name}'
print(greet('ada')) # HI ADAThree levels you should know
- Plain —
@timer: wraps and returns. - Parameterised —
@retry(times=3): a function that returns a decorator, so it needs one extra layer of nesting. - Class-based — a class with
__call__, useful when the decorator needs to hold state (like a call counter).
Always apply functools.wraps to the wrapper — without it your decorated function loses its real name, docstring and signature, which breaks debugging and documentation tools.
Example
Loading editor…
Press Run to execute the code.
When to use it
- A web framework applies @route('/home') decorators to mark functions as URL handlers.
- A @cache decorator wraps expensive computation functions so repeated calls return a stored result.
- A class-based decorator tracks call counts and timings across all decorated functions in a service.
More examples
Decorator with functools.wraps
Wraps a function to log each call and its return value, preserving the original function's metadata.
from functools import wraps
def log_call(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f'Calling {func.__name__}')
result = func(*args, **kwargs)
print(f'{func.__name__} returned {result!r}')
return result
return wrapper
@log_call
def add(a, b): return a + b
add(3, 4)
# Calling add
# add returned 7Parametrised decorator factory
A three-layer decorator factory that retries a function on specified exception types.
from functools import wraps
def retry(times=3, exceptions=(Exception,)):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == times:
raise
print(f'Retry {attempt}: {e}')
return wrapper
return decorator
@retry(times=3, exceptions=(ValueError,))
def risky(): raise ValueError('oops')
try: risky()
except ValueError: print('All retries exhausted')Class-based decorator
Implements a class-based decorator that counts how many times the wrapped function is called.
class CountCalls:
def __init__(self, func):
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
@CountCalls
def greet(name): print(f'Hello, {name}!')
greet('Alice')
greet('Bob')
print(greet.count) # 2
Discussion