asyncio
Python
coroutine
async programming
Python decorators

What should I decorate with asyncio.coroutine for async operations?

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

The short modern answer is: usually nothing. @asyncio.coroutine was the old way to mark a generator-based coroutine, and new async code should normally use async def instead.

The historical rule was simple, though. You decorated generator functions that cooperatively yielded control back to the event loop with yield from. You did not decorate ordinary synchronous helpers, CPU-bound work, or functions that still blocked the thread.

What @asyncio.coroutine Was For

Before native async and await syntax became the normal style, asyncio used generator-based coroutines. The decorator told Python and the event loop that the function was meant to behave like a coroutine.

Conceptually, it was for functions that:

  • performed non-blocking asynchronous steps
  • yielded control with yield from
  • were intended to be awaited by other coroutine code

In that legacy style, this was the kind of function you would decorate:

python
1import asyncio
2
3async def fetch_value():
4    await asyncio.sleep(0.1)
5    return 42
6
7async def main():
8    value = await fetch_value()
9    print(value)
10
11asyncio.run(main())

That example uses modern syntax because it is the recommended runnable form today. In older code, the same idea would have been expressed with a decorated generator coroutine instead of async def.

What You Should Not Decorate

A normal synchronous helper does not become non-blocking just because you add a coroutine decorator. If the function still performs blocking I/O, calls time.sleep, or burns CPU in a long loop, decorating it does not make it safe for the event loop.

That means you should not use coroutine syntax or decorators for:

  • plain utility functions that return immediately
  • blocking database or network calls that do not integrate with asyncio
  • CPU-heavy work that should run in another thread or process

For blocking work in modern asyncio code, use tools such as asyncio.to_thread or an executor instead of pretending the function is asynchronous.

python
1import asyncio
2import time
3
4
5def blocking_work():
6    time.sleep(1)
7    return "done"
8
9
10async def main():
11    result = await asyncio.to_thread(blocking_work)
12    print(result)
13
14asyncio.run(main())

The key idea is that async markers belong on functions that actually participate in cooperative asynchronous scheduling.

Think in Terms of Awaitable Boundaries

The best mental model is to decorate or define as async only the functions that form awaitable boundaries in your program. Those are the functions that other coroutine code should be able to await because they eventually yield control during I/O or another async step.

For example, in modern code you might write:

  • 'async def read_message(...)'
  • 'async def handle_client(...)'
  • 'async def fetch_user(...)'

But you would keep synchronous helpers as regular def functions if they only format strings, validate data, or perform quick pure-Python calculations.

That separation makes code easier to reason about and avoids unnecessary async noise.

Legacy Code Guidance

If you are maintaining older asyncio code that still uses generator-based coroutines, the decorator belongs on the generator functions that were written to yield from other awaitable operations. Even there, the long-term direction is usually migration to async def.

That migration is not just syntax cleanup. Modern syntax is clearer, better supported, and easier for new contributors to understand.

Common Pitfalls

  • Decorating a blocking function and assuming it is now non-blocking.
  • Mixing coroutine-style functions with synchronous code without knowing where the real await points are.
  • Decorating tiny helper functions that should stay ordinary def functions.
  • Confusing historical generator-based coroutines with modern async def coroutines.
  • Keeping legacy coroutine decoration in new code when native async and await is the clearer option.

Summary

  • In modern Python, new async code should usually use async def, not @asyncio.coroutine.
  • Historically, the decorator belonged on generator functions that cooperatively yielded control during async work.
  • Do not decorate blocking or CPU-bound functions and expect the event loop to stay responsive.
  • Keep simple synchronous helpers as normal functions.
  • Think of async markers as part of the boundary where code becomes awaitable.

Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Basis Lab
No course covers this one yet

Basis Lab writes one for you from a sentence about what you want to be able to do, then teaches it and checks you understood. Your first course is free.

Build my course
Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions