What Is a Decorator?
A decorator is a powerful Python pattern that adds functionality to an existing function or class without modifying it. You use it by placing the @ symbol above a function, and internally it is a higher-order function that takes a function as an argument and returns a new function.
It cleanly separates repetitive cross-cutting concerns such as logging, authentication checks, caching, and performance measurement. This article walks through how decorators work and practical patterns step by step.
How It Works — Functions Are First-Class Objects
In Python, functions are first-class objects. They can be assigned to variables, passed as arguments, and used as return values. Decorators leverage this characteristic.
# Pattern: take a function as an argument and return a new function
def my_decorator(func):
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} called") # Logic before the original function
result = func(*args, **kwargs)
print(f"[LOG] {func.__name__} finished") # Logic after the original function
return result
return wrapper
@my_decorator
def greet(name):
"""Returns a greeting message."""
return f"Hello, {name}!"
# Execution
print(greet("Alice"))
# [LOG] greet called
# [LOG] greet finished
# Hello, Alice!
@my_decorator is equivalent to greet = my_decorator(greet). The original greet function is replaced by the wrapper function, and logging logic is added before and after the call.
| Component | Role |
|---|---|
my_decorator | Decorator function — creates a wrapper around the original function |
wrapper | Wrapper function — the function that actually gets called |
func | Original function — called inside the wrapper |
*args, **kwargs | Passes all arguments to the original function as-is |
The Importance of functools.wraps
When a decorator is applied, the original function’s metadata (__name__, __doc__, etc.) gets overwritten with the wrapper’s metadata. functools.wraps solves this problem.
import functools
def my_decorator(func):
@functools.wraps(func) # Preserves the original function's metadata
def wrapper(*args, **kwargs):
print(f"[LOG] {func.__name__} called")
result = func(*args, **kwargs)
return result
return wrapper
@my_decorator
def greet(name):
"""Returns a greeting message."""
return f"Hello, {name}!"
# With wraps applied, metadata is preserved
print(greet.__name__) # greet (without wraps, this would be "wrapper")
print(greet.__doc__) # Returns a greeting message.
If you omit @functools.wraps(func), all function names appear as wrapper during debugging, making tracing difficult. It must always be used in production code.
Decorators with Arguments
To pass configuration values to a decorator itself, you need a triple-nested function structure. The outer function accepts the arguments and returns the actual decorator.
import functools
import time
def retry(max_attempts=3, delay=1.0):
"""Decorator that retries on failure"""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
last_exception = None
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except Exception as e:
last_exception = e
print(f"[RETRY] {func.__name__} failed "
f"({attempt}/{max_attempts}): {e}")
if attempt < max_attempts:
time.sleep(delay)
raise last_exception # Raise the last exception if all attempts fail
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def fetch_data(url):
"""Fetches data from an external API."""
import random
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError("Network error")
return {"status": "ok"}
# Execution (automatically retries on failure)
# [RETRY] fetch_data failed (1/3): Network error
# [RETRY] fetch_data failed (2/3): Network error
# {'status': 'ok'} <- Succeeds on the 3rd attempt
@retry(max_attempts=3) is equivalent to fetch_data = retry(max_attempts=3)(fetch_data). retry() is called first and returns decorator, which then wraps fetch_data.
Practical Pattern — Execution Time Measurement
import functools
import time
def timer(func):
"""Decorator that measures function execution time"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"[TIMER] {func.__name__}: {elapsed:.4f}s")
return result
return wrapper
@timer
def heavy_computation(n):
"""Simulates a heavy computation"""
return sum(i * i for i in range(n))
result = heavy_computation(1_000_000)
# [TIMER] heavy_computation: 0.0523s
Class-Based Decorators
A class that implements the __call__ method can also be used as a decorator. This is useful when you need to maintain state across calls.
import functools
class CountCalls:
"""Class decorator that tracks function call count"""
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.call_count = 0
def __call__(self, *args, **kwargs):
self.call_count += 1
print(f"[COUNT] {self.func.__name__}: "
f"call #{self.call_count}")
return self.func(*args, **kwargs)
@CountCalls
def say_hello(name):
return f"Hello, {name}!"
say_hello("Alice") # [COUNT] say_hello: call #1
say_hello("Bob") # [COUNT] say_hello: call #2
print(say_hello.call_count) # 2
Class decorators can naturally maintain state across calls, like call_count. With function decorators, you would need closure variables or function attributes, making the code more complex.
Combining Decorators (Stacking)
Multiple decorators can be applied simultaneously. They are applied from bottom to top and executed from top to bottom.
@timer # Applied 3rd, executed 1st
@retry(max_attempts=2) # Applied 2nd, executed 2nd
@CountCalls # Applied 1st, executed 3rd
def unstable_api_call():
import random
if random.random() < 0.5:
raise ConnectionError("Timeout")
return "Success"
# Execution order: timer -> retry -> CountCalls -> original function
The application order of the decorator stack is bottom to top, while the execution order is top to bottom. This is equivalent to unstable_api_call = timer(retry(max_attempts=2)(CountCalls(unstable_api_call))).
Summary
Decorators are a core Python pattern for cleanly separating repetitive cross-cutting concerns.
- Basic structure: A higher-order function that takes a function and returns a wrapper
functools.wraps: Always use it to preserve the original function’s metadata- Decorators with arguments: Triple-nested function structure (decorator factory)
- Class decorators: Use when state needs to be maintained across calls
- Practical uses: Widely used for logging, caching, retries, permission checks, performance measurement, and more
- Stack order: Applied bottom to top, executed top to bottom
When used effectively, decorators clearly separate business logic from auxiliary functionality, greatly improving maintainability.