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
import functools
# 1) Parameterised decorator: retry on failure
def retry(times, default=None):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except ValueError as e:
print(f' attempt {attempt} failed: {e}')
return default
return wrapper
return decorator
# 2) Class-based decorator that keeps state across calls
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.count = 0
def __call__(self, *args, **kwargs):
self.count += 1
return self.func(*args, **kwargs)
attempts = iter([ValueError('nope'), ValueError('again'), 'success'])
@CountCalls
@retry(times=3, default='gave up')
def fetch():
result = next(attempts)
if isinstance(result, Exception):
raise result
return result
print('result :', fetch()) # succeeds on 3rd internal attempt
print('call count:', fetch.count) # 1 outer call
print('name kept :', fetch.__wrapped__.__name__) # fetch
# Output:
# attempt 1 failed: nope
# attempt 2 failed: again
# result : success
# call count: 1
# name kept : fetchWhen 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