Using a coroutine as decorator
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
In Python async code, people often say they want to “use a coroutine as a decorator,” but the practical pattern is more specific than that. You usually write a normal decorator function that returns an async wrapper, because decoration happens at import time while coroutine execution happens later inside the event loop.
Core Sections
The key distinction: decorator definition versus coroutine execution
A decorator runs when the function is defined. An async def function does not execute immediately; calling it creates a coroutine object that must later be awaited. That means a bare coroutine object cannot directly act as a normal decorator in the usual way.
The common solution is:
- define a regular decorator function
- inside it, return an async wrapper
- '
awaitthe wrapped coroutine inside that wrapper'
This is the idiomatic approach for decorating async functions.
Why a normal synchronous decorator is still correct
It can feel counterintuitive that the decorator itself is not async. The reason is timing. At decoration time, there is no running event loop requirement and nothing to await yet. The decorator only needs to build and return another callable.
That callable can absolutely be async, which is what preserves async behavior for the original function.
If you wrote this instead:
then @broken_decorator would not behave like a normal decorator. Python would call broken_decorator(func) during function definition and get a coroutine object back, not the wrapped function you intended.
Add timing, retries, or tracing around async work
Async decorators are useful when the extra behavior belongs around many coroutine calls. Timing is a common example.
Because the wrapper awaits the original coroutine, the timing includes the real async execution rather than just the function call that created the coroutine object.
A retry decorator follows the same pattern.
This is a decorator factory, which is the right pattern when you need configuration.
Preserve metadata and cancellation behavior
Use functools.wraps unless you have a strong reason not to. Without it, the decorated function loses its original name and docstring, which hurts debugging and introspection.
Cancellation is also important in async code. If the wrapped coroutine is cancelled, do not swallow that exception accidentally. In many async systems, cancellation is part of normal control flow and should propagate.
That preserves cooperative cancellation instead of converting it into a misleading success path.
Keep sync and async decorators separate when needed
A decorator written for coroutines should generally decorate coroutines only. If you apply it to a synchronous function, callers must now await the wrapper, which changes the API. If you need both modes, either create separate decorators or inspect the target with inspect.iscoroutinefunction and branch deliberately.
Clarity matters more than cleverness here. A small explicit decorator is easier to maintain than a generic wrapper that surprises callers.
Common Pitfalls
- Defining the decorator itself as
async def, which returns a coroutine object at decoration time instead of a callable wrapper. - Decorating a synchronous function with an async wrapper and accidentally changing how callers must invoke it.
- Forgetting
@wraps, which makes logs, stack traces, and documentation point towrapperinstead of the original function. - Catching broad exceptions in the wrapper and swallowing
asyncio.CancelledError, which breaks normal cancellation flow. - Performing blocking work inside the async wrapper, which defeats the purpose of using coroutines in the first place.
Summary
- The practical pattern is a normal decorator that returns an async wrapper.
- Decoration happens at definition time, while coroutine execution happens later in the event loop.
- Async decorators are useful for logging, timing, retries, and tracing around coroutine calls.
- Use
functools.wrapsand let cancellation propagate unless you have a strong reason not to. - Keep sync and async decoration behavior explicit so callers are not surprised.

