Using decorator @-notation is Python’s cleanest, most idiomatic way to apply decorators — the @ symbol placed directly above a function (or class) definition replaces the traditional manual wrapping: func = decorator(func). It’s syntactic sugar that makes code more readable, declarative, and self-documenting, especially when stacking multiple decorators or using parameterized ones. In 2026, @-notation is universal — every major library (FastAPI, Flask, pytest, Typer, Pydantic, Celery, pandas/Polars extensions) uses it for logging, timing, caching, auth, validation, retry, dependency injection, and more. Mastering @-notation (and its nuances with arguments) lets you write concise, professional, production-ready Python code that clearly signals “this function is enhanced” at a glance.
Here’s a complete, practical guide to using decorator @-notation in Python: basic application, stacking decorators, parameterized decorators with @, equivalent manual form, real-world patterns, and modern best practices with type hints, functools.wraps, order matters, and pandas/Polars integration.
Basic @-notation — applies the decorator directly to the function definition.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def say_hello(name: str) -> str:
"""Simple greeting."""
return f"Hello, {name}!"
say_hello("Alice")
# Before
# After
# Hello, Alice!
Equivalent manual form — @-notation is exactly this under the hood.
def say_hello(name: str) -> str:
return f"Hello, {name}!"
say_hello = my_decorator(say_hello) # same as @my_decorator
Parameterized decorators with @ — the @ line calls the factory, which returns the real decorator.
def repeat(times: int):
def decorator(func):
def wrapper(*args, **kwargs):
for _ in range(times):
func(*args, **kwargs)
return wrapper
return decorator
@repeat(times=3)
def greet(name: str) -> None:
print(f"Hi, {name}!")
greet("Bob")
# Hi, Bob!
# Hi, Bob!
# Hi, Bob!
Stacking decorators — multiple @ lines apply from bottom to top (order matters).
@timer
@logging
@repeat(times=2)
def process_data(df: pd.DataFrame) -> pd.DataFrame:
return df.groupby('category').sum()
# Execution order: repeat ? logging ? timer ? process_data
Best practices make @-notation safe, readable, and powerful. Always use @functools.wraps in decorators — preserves __name__, __doc__, __annotations__. Modern tip: use Polars method chaining — df.lazy().filter(...).group_by(...).agg(...).collect() — native composition, often faster than custom decorators. Add type hints — def decorator(func: Callable[P, R]) -> Callable[P, R] — preserves signatures. Use order matters — bottom decorator runs first (innermost). Prefer parameterized factories — @retry(3) over global config. Document stacked decorators — explain order in docstring. Test decorated functions — assert original behavior preserved. Use @contextmanager — for decorators that double as context managers. Combine with fastapi/typer — @-notation for dependencies/routes/CLI commands. Avoid deep stacking — 3–4 max; refactor complex logic. Use decorator library — advanced wrapping tools if needed. Profile decorated code — @timer or line_profiler on wrapped functions.
Using decorator @-notation applies wrappers cleanly and declaratively — basic @, parameterized @factory, stacking from bottom up. In 2026, preserve metadata with @wraps, use type hints, prefer Polars chaining for data, document order, and test thoroughly. Master @-notation, and you’ll write concise, readable, professional Python code that clearly signals enhanced behavior.
Next time you need to enhance a function — use @. It’s Python’s cleanest way to say: “Decorate this — make it better without changing it.”