Decorators look like a simple @ symbol placed just above a function (or class) definition, but they are one of Python’s most elegant and powerful syntactic features. Under the hood, @decorator is just shorthand for function = decorator(function) — it takes the function being defined, passes it to the decorator, and rebinds the name to whatever the decorator returns (usually a wrapper function that adds behavior). In 2026, decorators remain ubiquitous — they power logging, timing, caching, authentication, validation, retry logic, rate limiting, memoization, and observability in web frameworks (FastAPI, Flask), data pipelines, ML training, and production systems. Understanding how decorators “look like” (syntax) and work (wrapping) unlocks cleaner, more reusable, and maintainable code by separating cross-cutting concerns from business logic.
Here’s a complete, practical guide to how decorators look and work in Python: basic syntax, manual decoration vs @, writing simple decorators, preserving metadata with @wraps, parameterized decorators, stacked decorators, real-world patterns, and modern best practices with type hints, functools, performance, and pandas/Polars integration.
Basic decorator syntax — @ above the function definition applies the decorator automatically.
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
Manual decoration (equivalent to @) — shows what the decorator actually does.
def say_hello():
print("Hello, World!")
# Same as @my_decorator
say_hello = my_decorator(say_hello)
say_hello() # same output as above
Preserving function metadata — without @wraps, wrapped function loses name, docstring, annotations.
from functools import wraps
def my_decorator(func):
@wraps(func) # crucial — copies metadata
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.
print(greet("Alice")) # Hello, Alice!
Parameterized decorator — 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: timing decorator for pandas/Polars functions — monitor performance 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 group_and_sum(df: pd.DataFrame) -> pd.DataFrame:
return df.groupby('category').sum()
# Usage
df = pd.DataFrame({'category': ['A','A','B'], 'value': [10,20,30]})
result = group_and_sum(df)
# group_and_sum took 0.0012 seconds
Best practices make decorators safe, readable, and performant. Always use @wraps(func) — preserves name, docstring, annotations, type hints. 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. 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. Stack decorators thoughtfully — order matters (@timer @log vs @log @timer).
Decorators look like @name above a function — they wrap and extend behavior without modifying source code. In 2026, use @wraps, type hints, parameterized factories, stacked decorators, and Polars for high-performance pipelines. Master decorators, and you’ll write clean, reusable, observable Python code that adds cross-cutting features elegantly and efficiently.
Next time you need logging, timing, caching, or validation around a function — write a decorator. It’s Python’s cleanest way to say: “Enhance this function without touching its code.”