Decorators in Python
What is a Decorator?
A decorator is a function that wraps another function to extend or modify its behavior — without changing the original function's code. They use the @ syntax.
How Decorators Work
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before the function")
result = func(*args, **kwargs)
print("After the function")
return result
return wrapper
# Apply with @ syntax
@my_decorator
def say_hello(name: str):
print(f"Hello, {name}!")
say_hello("Alice")
# Before the function
# Hello, Alice!
# After the function
# Equivalent to:
# say_hello = my_decorator(say_hello)
Preserving Function Metadata
from functools import wraps
def my_decorator(func):
@wraps(func) # preserves __name__, __doc__, etc.
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def greet(name: str) -> str:
"""Greet someone by name."""
return f"Hello, {name}!"
print(greet.__name__) # greet (not 'wrapper')
print(greet.__doc__) # Greet someone by name.
Practical Decorator Examples
import time
from functools import wraps
# Timer decorator
def timer(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
end = time.perf_counter()
print(f"{func.__name__} took {end - start:.4f}s")
return result
return wrapper
@timer
def slow_function():
time.sleep(0.1)
return "done"
slow_function() # slow_function took 0.1001s
# Logger decorator
def log_calls(func):
@wraps(func)
def wrapper(*args, **kwargs):
print(f"Calling {func.__name__}({args}, {kwargs})")
result = func(*args, **kwargs)
print(f"{func.__name__} returned {result}")
return result
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
add(3, 5)
# Calling add((3, 5), {})
# add returned 8
# Retry decorator
def retry(times: int = 3):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except Exception as e:
print(f"Attempt {attempt} failed: {e}")
raise RuntimeError(f"Failed after {times} attempts")
return wrapper
return decorator
@retry(times=3)
def unstable_api():
import random
if random.random() < 0.7:
raise ConnectionError("Network error")
return "Success"
Stacking Decorators
from functools import wraps
def bold(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<b>{func(*args, **kwargs)}</b>"
return wrapper
def italic(func):
@wraps(func)
def wrapper(*args, **kwargs):
return f"<i>{func(*args, **kwargs)}</i>"
return wrapper
# Applied bottom-up: italic first, then bold
@bold
@italic
def greet(name: str) -> str:
return f"Hello, {name}!"
print(greet("Alice")) # <b><i>Hello, Alice!</i></b>
# Class-based decorator
class Cache:
def __init__(self, func):
wraps(func)(self)
self.func = func
self._cache = {}
def __call__(self, *args):
if args not in self._cache:
self._cache[args] = self.func(*args)
return self._cache[args]
@Cache
def fibonacci(n: int) -> int:
if n <= 1:
return n
return fibonacci(n - 1) + fibonacci(n - 2)
print(fibonacci(35)) # fast due to caching