Back to Blog
Python

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...

async-awaitasynciocoroutinesevent-loopruntime-warnings
Diagram showing a suspended coroutine object awaiting execution by an event loop

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:

MethodContextBehavior
await coroInside async functionRuns inline, blocks until completion
asyncio.run(coro)Sync code / top levelCreates event loop, runs to completion
asyncio.create_task(coro)Inside async functionSchedules for concurrent execution
coro.close()AnywhereMarks 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 object
  • cr_frame: the current execution frame, or None if not started
  • cr_running: whether the coroutine is currently executing
  • cr_await: the object currently being awaited, or None
  • cr_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.

python coroutine object: Practical Usage and Code Examples | RYUSLOG DEV