Decorators
A function that wraps another function to extend or modify its behavior. @decorator is syntactic sugar for func = decorator(func).
Decorators = gift wrapping. @decorator wraps your function in fancy paper at definition time. Stacked decorators? The bottom wrapper goes on first, then the outer one wraps around it — like nesting dolls.
@decorator runs at definition time, not call time. The returned wrapper replaces the original function. Decorators are commonly used for logging, auth, caching (@functools.lru_cache), retries, and timing. Always use @functools.wraps(func) in the wrapper to preserve __name__, __doc__, and __wrapped__.
Decorators can be stacked — @A @B def f() is f = A(B(f)), applied bottom-up. Decorators with arguments need an extra wrapper layer: decorator_factory(arg) returns the actual decorator. Class-based decorators implement __call__. @functools.wraps copies over __module__, __name__, __qualname__, __doc__, __dict__, __annotations__, and sets __wrapped__. Without it, introspection tools (sphinx, pytest fixtures) break. Descriptor-based decorators (like @property) are a different mechanism entirely.
A decorator is a callable that takes a function and returns a new one. @my_decorator on a function is exactly my_decorator(func) — happens at import/definition time. Stacked decorators apply bottom-up. Always use @functools.wraps to preserve the wrapped function's metadata — without it you'll break introspection and debugging. For decorators that take arguments, you need one extra level of nesting: the outer callable takes args and returns the actual decorator.
Stacked decorators apply bottom-up, not top-down. @A @B def f() → f = A(B(f)). B wraps f first. Also: decorators with arguments like @retry(3) vs @retry — the former calls retry(3) which must return a decorator, the latter passes the function directly.