Menu

Python Decorators: @functions, Arguments, and functools.wraps

What Python decorators actually are, how to write your own, and the patterns (arguments, stacking, wraps) that make them worth using.

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:

main.py
Output
Click Run to see the output here.

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:

main.py
Output
Click Run to see the output here.

@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:

main.py
Output
Click Run to see the output here.

*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:

main.py
Output
Click Run to see the output here.

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:

main.py
Output
Click Run to see the output here.

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.

main.py
Output
Click Run to see the output here.

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.

main.py
Output
Click Run to see the output here.

Three layers sounds like a lot. Read outward:

  1. repeat(times=3) is a function call. It returns the decorator.
  2. decorator is the actual decorator — it takes a function and returns a wrapped one.
  3. wrapper is 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:

main.py
Output
Click Run to see the output here.

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:

main.py
Output
Click Run to see the output here.
  • @property turns a method into a computed attribute.
  • @staticmethod marks a method that doesn't use self or cls.
  • @classmethod receives the class as cls instead of an instance — great for alternative constructors.
  • @functools.lru_cache memoizes 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.

Learn to code with Coddy

GET STARTED