Decorators are one of Python’s most elegant and powerful features — they let you modify or extend the behavior of functions (or classes) without changing their source code. A decorator is a higher-order function that takes another function as input, wraps it with additional logic, and returns the wrapped version. The @ syntax is just syntactic sugar for applying the decorator. In 2026, decorators remain ubiquitous — they power logging, timing, caching, authentication, validation, retry logic, rate limiting, and more in web frameworks (Flask/FastAPI), data pipelines, ML training loops, and production systems. Mastering decorators unlocks cleaner, more reusable, and maintainable code by separating concerns and avoiding boilerplate.
Here’s a complete, practical guide to decorators in Python: basic syntax and mechanics, writing your own decorators, parameterized decorators, stacked decorators, real-world patterns, and modern best practices with type hints, functools.wraps, performance, and pandas/Polars integration.
Basic decorator — wraps a function to add behavior before/after execution.
def my_decorator(func):
def wrapper():
print("Before function call")
func()
print("After function call")
return wrapper
@my_decorator
def say_hello():
print("Hello, World!")
say_hello()
# Before function call
# Hello, World!
# After function call
Preserving function metadata with functools.wraps — without it, wrapped function loses name, docstring, annotations.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def greet(name: str) -> str:
"""Greet someone."""
return f"Hello, {name}!"
print(greet.__name__) # greet (not wrapper)
print(greet.__doc__) # Greet someone.
greet("Alice") # works with args
Parameterized decorators — use a factory function to accept parameters and return the actual decorator.
def repeat(times: int):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(3)
def say_hi():
print("Hi!")
say_hi()
# Hi!
# Hi!
# Hi!
Real-world pattern: logging/timing decorator for pandas/Polars pipelines — monitor function execution without cluttering business logic.
import time
from functools import wraps
import pandas as pd
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} seconds")
return result
return wrapper
@timer
def process_data(df: pd.DataFrame) -> pd.DataFrame:
return df.groupby('category').agg({'value': 'sum'})
# Usage
df = pd.DataFrame({'category': ['A','A','B'], 'value': [10,20,30]})
result = process_data(df)
# process_data took 0.0012 seconds
Best practices make decorators safe, readable, and performant. Always use @wraps(func) — preserves metadata (name, docstring, annotations). Use parameterized decorators via factory functions — @repeat(3). Modern tip: use Polars for high-performance transformations — decorate Polars lazy frames or expressions for timing/logging. Add type hints — def decorator(func: Callable) -> Callable — improves clarity and mypy checks. Use functools.wraps — essential for introspection. Prefer simple decorators — avoid deep nesting; compose with multiple @ if needed. Handle *args, **kwargs — makes decorators generic. Return the wrapped result — chainable decorators. Use contextlib.ContextDecorator — class-based decorators that work as context managers. Test decorators independently — wrap dummy functions and assert behavior. Combine with tenacity — retry decorators for transient errors. Use logging inside decorators — centralize logging without cluttering functions. Avoid decorators on generators — can break iteration; use wrappers carefully.
Decorators wrap functions to add behavior (logging, timing, caching, auth) without modifying source code — use @wraps, type hints, factory pattern for parameters, and Polars for high-performance pipelines. In 2026, decorators are everywhere — master them to write clean, reusable, observable Python code that separates concerns and adds cross-cutting features elegantly.
Next time you need to add logging, timing, or validation around a function — write a decorator. It’s Python’s cleanest way to say: “Enhance this function without touching its code.”