Python Coroutine Function: Syntax and Behavior
python coroutine function: Learn how Python coroutine functions work, how to define them with async def, and how they differ from generators.
A Python coroutine function is defined with async def. When you call it, the body does not run immediately. Instead, the call returns a coroutine object that must be awaited or scheduled on an event loop. This distinction between function definition and execution is the foundation of asynchronous programming in Python.
Defining a Coroutine Function with async def
The syntax for a coroutine function is straightforward:
async def fetch_data(): return {"status": "ok"}
The async keyword marks the function as a coroutine function. Calling fetch_data() does not execute the body; it returns a coroutine object:
coro = fetch_data() print(type(coro)) # <class 'coroutine'>
To run the body, you must either await the coroutine from within another coroutine, or pass it to an event loop runner like asyncio.run().
How a Coroutine Function Differs from a Generator
A generator function uses yield to produce a sequence of values lazily. A coroutine function uses async def and await to suspend and resume execution at specific points. The key difference is the purpose: generators produce values, while coroutines suspend for I/O or other asynchronous operations.
def count_up_to(n): for i in range(n): yield i async def wait_and_return(): await asyncio.sleep(1) return "done"
Generators are driven by iteration; coroutines are driven by an event loop. A generator can be a coroutine if it uses async def and await, but a plain generator is not a coroutine function.
Awaiting a Coroutine Function
To execute a coroutine function, you use await inside another coroutine, or you schedule it with asyncio.run() for a top-level call.
import asyncio async def main(): result = await fetch_data() print(result) asyncio.run(main())
await suspends the current coroutine until the awaited coroutine completes. The event loop can then run other tasks while the awaited operation is pending.
Coroutine Function Execution and Suspension
When a coroutine function is called, it returns a coroutine object immediately. The body does not start until the coroutine is awaited or scheduled. At each await expression, the coroutine yields control back to the event loop, which can resume it when the awaited operation finishes.
async def step1(): print("step1 start") await asyncio.sleep(1) print("step1 end") async def step2(): print("step2 start") await asyncio.sleep(0.5) print("step2 end") async def main(): await asyncio.gather(step1(), step2()) asyncio.run(main())
This allows concurrent execution of multiple coroutines without threads. The event loop interleaves their execution at suspension points.
Common Pitfalls with Coroutine Functions
One frequent mistake is calling a coroutine function without awaiting it, which produces a warning and does nothing:
fetch_data() # RuntimeWarning: coroutine was never awaited
Another issue is using a blocking call like time.sleep() inside a coroutine. This blocks the entire event loop, defeating the purpose of concurrency. Use asyncio.sleep() instead.
Mixing generators and coroutines can also cause confusion. A function that contains both yield and async def is invalid; you must choose one model.
Performance and Concurrency Considerations
Coroutine functions are lightweight compared to threads. Each coroutine has its own stack and state, but the overhead of switching between them is minimal because the event loop controls the switch points. However, coroutines do not provide parallelism; they run on a single thread. CPU-bound work inside a coroutine will block other tasks. For CPU-bound operations, consider using processes or threads with an executor.
The key performance benefit of coroutine functions is efficient I/O-bound concurrency. Many network calls can be awaited concurrently, allowing a single thread to handle thousands of connections.
When to Use a Coroutine Function
Use a coroutine function when you need to perform asynchronous I/O operations such as HTTP requests, database queries, or file reads without blocking the event loop. If your workload is CPU-bound, coroutines are not the right tool; use multiprocessing or threads. For simple sequential code, a regular function is simpler and easier to debug.
Coroutine functions are also useful for building state machines or pipelines where you need to suspend and resume based on external events. Libraries like asyncio and aiohttp rely on them for non-blocking network operations.