A Decorator Is a Function That Wraps a Function
That sentence sounds abstract, but the mechanics are simple. A decorator takes a function in, returns a function out. The function it returns usually calls the original one, with some extra behavior wrapped around the call.
The shortest possible example:
shout is the decorator. It takes a function (greet), builds a new function (wrapper) that calls the original and uppercases the result, and returns it. Reassigning greet = shout(greet) swaps the original for the wrapped version.
That reassignment pattern is so common that Python gave it a dedicated syntax.
The @ Is Sugar for a Reassignment
@name on the line above a def is equivalent to name = name(...) right after the function is defined:
@shout reads as "apply the shout decorator to this function." Python runs greet = shout(greet) immediately after the def — same mechanics as before, less typing.
Once you see @name, mentally replace it with function = name(function). That's all the syntax means.
Handling Arguments
Most functions take arguments. A usable decorator passes them through. The idiom is *args, **kwargs — Python's way of accepting any arguments — because the wrapper shouldn't care what the wrapped function expects:
*args captures all positional arguments. **kwargs captures all keyword arguments. The wrapper forwards everything to the wrapped function unchanged, then does whatever extra work the decorator is for — here, uppercasing the result.
This is the shape most real decorators take.
A More Useful Example: Timing
Print how long a function takes:
The pattern — run something before the call, run something after — is what most decorators end up doing. Logging, auth checks, retries, and input validation all follow the same shape.
Preserving the Original Identity: functools.wraps
Decorating a function replaces it, which means the wrapped function loses its original __name__ and __doc__ attributes:
greet.__name__ is now "wrapper" and the docstring is gone. That breaks help(), tracebacks, and any tool that inspects the function.
The fix is one line: @functools.wraps(func) on the inner function copies the metadata across.
Always add @wraps(func) to the inner function. It costs nothing and avoids surprising debugging sessions later.
Decorators With Arguments
Sometimes the decorator itself needs configuration — "retry this function up to 3 times," "log at level DEBUG." That means one more layer of nesting: an outer function that takes the arguments and returns a decorator.
Three layers sounds like a lot. Read outward:
repeat(times=3)is a function call. It returns thedecorator.decoratoris the actual decorator — it takes a function and returns a wrapped one.wrapperis the wrapped function that runs at call time.
This shape powers @retry(times=5), @cache(maxsize=100), and framework decorators like @app.route("/users"). Once you see the three-layer pattern, the whole family reads the same way.
Stacking Decorators
You can apply more than one decorator to a function. They stack bottom-up — the one closest to the def runs first:
add_exclaim wraps first, adding the !. Then shout wraps that, uppercasing everything. The output is HI, ROSA!.
Order matters. Flipping the stack gives you HI, ROSA! with the exclaim added after uppercasing — visually identical here, but imagine a decorator that formats JSON: running it before or after a decorator that logs input can produce very different results.
Built-In Decorators You'll See
Python and its standard library ship with a handful of decorators you'll meet in real code:
@propertyturns a method into a computed attribute.@staticmethodmarks a method that doesn't useselforcls.@classmethodreceives the class asclsinstead of an instance — great for alternative constructors.@functools.lru_cachememoizes results, so repeated calls with the same arguments hit a cache.
Framework decorators (@app.route, @pytest.fixture, @dataclass) follow the same machinery. Nothing special — just functions that wrap functions.
When to Write One, When Not To
Write a decorator when you want to apply the same behavior to many functions — timing, logging, retries, authorization checks. The whole point is that the behavior stays out of the function's body.
Skip the decorator when:
- The behavior belongs to one specific function. Put it in the function.
- You only want it for testing. A fixture or a parameter is clearer.
- You're tempted to stack four or five of them. At that point, the control flow is hidden in the decorator chain — a reader has to unwind every layer to see what actually runs. A straightforward helper function can read better.
Decorators are a sharp tool. Used well, they keep code DRY and the intent obvious. Used poorly, they hide what the program is doing. Lean toward "obvious" when you're deciding whether to reach for one.
Next: Type Hints
Decorators are a common place to meet type hints in the wild — the wrapper functions often annotate their signatures. Type hints are a small feature that pays off fast, and they're coming up next.
Frequently Asked Questions
What is a decorator in Python?
A decorator is a function that takes another function and returns a new function — usually one that wraps the original with extra behavior. You apply one with @decorator_name on the line above a def. The @ syntax is shorthand for func = decorator_name(func).
What are decorators used for in Python?
Adding behavior around a function without editing its body — logging, timing, caching, authentication checks, input validation, retries. Frameworks use them heavily: @app.route(...) in Flask, @pytest.fixture in pytest, @property and @staticmethod built in.
Can I write my own decorator?
Yes. A decorator is just a function that takes a function and returns a function. Most custom decorators wrap the original call in a small inner function that does something before, after, or around it. Use functools.wraps on the inner function to preserve the original function's name and docstring.