What are decorators in Python, and how does the @ syntax work?
A decorator is a function that takes another function and gives you back a new one. The @ symbol is only a short way of writing that reassignment by hand, so putting @logged above a function definition does exactly the same thing as writing logged above it and then adding func = logged(func) on the next line.
The usual shape of a decorator has three parts. The outer function receives the function that is being decorated. Inside it sits a nested function that does the real work, which means calling the original function and then adding whatever the decorator is for, such as logging, timing, or a permission check. That nested function is then returned, so it takes the place of the original in the namespace.
Two details make this pattern work in real code. The wrapper is written with *args and **kwargs so that it accepts any call signature and passes everything through to the original function, because a decorator that hard-codes the parameters only works on functions that have exactly that shape. The wrapper is also normally marked with @functools.wraps(func), which copies the name, the docstring, and the type hints of the original function onto the wrapper so that the decorated function still looks like itself.
The earlier questions in this topic covered functions and their arguments, and this answer builds on them. You will meet decorators constantly in real work. Logging and timing wrappers, caching with functools.cache, route handlers inside web frameworks, and the retry logic inside software kits are all built this way. Understanding the pattern is what lets you read any of them without being afraid of them.