A decorator is a callable that takes a function and returns a replacement, using a closure to capture it
A decorator is a higher-order function: it accepts a callable and returns a callable that is bound to the original name. The @decorator syntax is pure sugar applied at definition time, so @logged above def add(...) is exactly add = logged(add). The wrapper function works because of a closure: it references the parameter fn from the enclosing scope, and Python stores that reference in a cell on the wrapper's closure so the original function stays alive after logged returns. This is what lets the wrapper run code before and after the wrapped call without the caller knowing.
Order matters: decorators apply bottom-up. @a above @b means a(b(f)), so the topmost decorator is the outermost wrapper.
Use functools.wraps inside the wrapper so the replacement keeps the original metadata, otherwise you break docs, debugging, and introspection-based frameworks.
Trade-off: decorators add a call frame per invocation and can hide control flow. Explicit helper functions or context managers are sometimes clearer and easier to test.
Common mistake: forgetting to return the wrapper, which silently rebinds the name to None and produces TypeError: 'NoneType' object is not callable.
Common mistake: forgetting *args, **kwargs, which makes the decorated function reject any arguments it actually accepts.
Version note: PEP 318 introduced decorator syntax in Python 2.4, class decorators came in 2.6, and the wrapped attribute was added in 3.2.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience