Understanding the Python Coroutine Object
python coroutine object: Learn what a Python coroutine object is, why async function bodies don't run immediately, and how to consume, close, and debug coroutines corr...
When you call a function defined with async def, Python does not execute the function body. Instead, it returns a python coroutine object. This object represents a computation that has not started yet, suspended until something drives it.
async def fetch_data(): return 42 result = fetch_data() print(type(result)) # <class 'coroutine'>
The result variable holds a coroutine object, not the integer 42. The body of fetch_data has not run at this point. This behavior is the source of many beginner errors and a few subtle runtime warnings.
Why the Function Body Doesn't Run Immediately
A coroutine object is created by the call, but its body is only executed when the coroutine is awaited or otherwise driven by an event loop. This is a deliberate design decision. It allows the caller to decide when and how the coroutine runs, and it enables the event loop to interleave multiple coroutines.
import asyncio async def fetch_data(): print("body executing") return 42 coro = fetch_data() # no output yet print("after call") # Output: # after call
Only when the coroutine is awaited does the body run:
async def main(): result = await coro print(result) asyncio.run(main()) # Output: # body executing # 42
The suspension model is what makes cooperative concurrency possible. Each await point inside the body yields control back to the event loop, allowing other tasks to make progress.
The "Never Awaited" RuntimeWarning
If a coroutine object is garbage collected without ever being awaited, Python emits a RuntimeWarning:
import asyncio async def fetch_data(): return 42 fetch_data() # RuntimeWarning: coroutine 'fetch_data' was never awaited
This warning appears when the coroutine object is destroyed, not at the call site. In interactive sessions it may appear immediately; in scripts it often appears at interpreter shutdown. The warning is a signal that you created a coroutine and then discarded it without running it, which usually means you forgot an await or misused the API.
The fix is almost always to await the coroutine, schedule it as a task, or explicitly close it if you no longer need it.
Consuming a Coroutine Object
There are several ways to consume a coroutine object, and the right choice depends on the context.
Awaiting Inside an Async Function
Inside an async def function, use await:
async def main(): coro = fetch_data() result = await coro print(result)
Running From Synchronous Code
At the top level or from sync code, use asyncio.run():
result = asyncio.run(fetch_data()) print(result)
asyncio.run() creates a new event loop, runs the coroutine to completion, and closes the loop.
Scheduling as a Task
When you want the coroutine to run concurrently with other work, wrap it in a task:
async def main(): task = asyncio.create_task(fetch_data()) # other work here result = await task print(result)
The table below summarizes the common consumption methods:
| Method | Context | Behavior |
|---|---|---|
await coro | Inside async function | Runs inline, blocks until completion |
asyncio.run(coro) | Sync code / top level | Creates event loop, runs to completion |
asyncio.create_task(coro) | Inside async function | Schedules for concurrent execution |
coro.close() | Anywhere | Marks coroutine as closed, no execution |
Inspecting Coroutine Object Attributes
Coroutine objects expose a few attributes that are useful for debugging and introspection:
async def fetch_data(): await asyncio.sleep(1) return 42 coro = fetch_data() print(coro.cr_code) # code object for fetch_data print(coro.cr_running) # False print(coro.cr_await) # None before first await
cr_code: the underlying code objectcr_frame: the current execution frame, orNoneif not startedcr_running: whether the coroutine is currently executingcr_await: the object currently being awaited, orNonecr_origin: where the coroutine was created (if tracing is enabled)
These attributes are implementation details of CPython and are not part of the language specification. They are useful in debugging sessions and diagnostic tools, but production code should not rely on them for control flow.
Closing and Cancelling Coroutines
If you create a coroutine object and decide not to run it, call close() to avoid the "never awaited" warning:
coro = fetch_data() coro.close()
After close(), the coroutine is marked as closed and cannot be awaited. Attempting to await it raises RuntimeError: cannot reuse already awaited coroutine.
For tasks created with asyncio.create_task(), cancellation is handled differently. Calling task.cancel() schedules a CancelledError to be raised inside the coroutine at the next await point:
task = asyncio.create_task(fetch_data()) task.cancel() try: await task except asyncio.CancelledError: pass
This is the proper way to stop a running coroutine. close() only works on coroutine objects that have not started executing.
Common Mistakes and Debugging
The most frequent mistake is treating an async function like a regular function:
def main(): result = fetch_data() # result is a coroutine object, not the return value print(result) # prints <coroutine object fetch_data at ...>
Another common issue is awaiting the same coroutine twice. A coroutine object can only be awaited once:
coro = fetch_data() await coro await coro # RuntimeError: cannot reuse already awaited coroutine
If you need the same result multiple times, call the async function again or store the result after the first await.
When debugging, the cr_origin attribute can help trace where a leaked coroutine was created. Enable tracing with:
import sys sys.set_coroutine_origin_tracking_depth(2)
This adds origin information to coroutine objects, which shows up in the "never awaited" warning and in cr_origin.