Runtimewarning Coroutine Main Was Never Awaited

5 min read

Encountering a RuntimeWarning: coroutine 'main' was never awaited is a rite of passage for almost every Python developer venturing into asynchronous programming. It usually appears when you run a script expecting it to execute an async def function, but the script finishes instantly without doing the actual work. The warning is Python’s way of telling you: "You created an asynchronous object, but you forgot to actually run it But it adds up..

This article breaks down exactly why this happens, the mechanics of coroutines versus tasks, and the standard patterns to fix it across different Python versions.

Understanding the Core Concept: Coroutines Are Inert

Before diving into solutions, it is crucial to understand what a coroutine object actually is. When you define a function with async def, calling that function does not execute the code inside it. Instead, it returns a coroutine object Less friction, more output..

Think of a coroutine object like a cold engine. It has all the potential to run, the logic is defined, but the pistons aren't moving. The await keyword (or the event loop) is the ignition key. Without it, the code inside the async def block remains completely dormant Most people skip this — try not to. Which is the point..

Real talk — this step gets skipped all the time.

Consider this minimal example that triggers the warning:

import asyncio

async def main():
    print("Hello from async world!")

# The mistake: calling main() without await or asyncio.run()
main() 

Output:

sys:1: RuntimeWarning: coroutine 'main' was never awaited

Notice that "Hello from async world!The script created the coroutine object, held it in a temporary variable (which was immediately discarded), and exited. That said, " never prints. Python’s garbage collector then cleaned up the object and emitted the warning because the coroutine was destroyed while still in a "pending" state.

The Standard Fix: asyncio.run()

Since Python 3.On top of that, run(). 7, the canonical way to execute a top-level entry point is asyncio.This function handles the heavy lifting: it creates a new event loop, runs the passed coroutine until completion, and closes the loop cleanly And it works..

Corrected Code:

import asyncio

async def main():
    print("Hello from async world!")

if __name__ == "__main__":
    asyncio.run(main())

Why this works:

  1. main() creates the coroutine object.
  2. asyncio.run(...) accepts that object, spins up the event loop, and drives the coroutine to completion.
  3. The print statement executes.
  4. The loop shuts down.

This is the only boilerplate you need for 95% of modern Python scripts (3.7+). It abstracts away the manual loop management that caused so much confusion in earlier versions.

Legacy Patterns: Python 3.6 and Older

If you are maintaining a legacy codebase stuck on Python 3.Even so, 6 or earlier, asyncio. In real terms, run() does not exist. That said, you must manage the event loop manually. This is where many developers historically got tripped up, leading to the "never awaited" warning or RuntimeError: Event loop is closed errors Small thing, real impact..

The Old Way (Python 3.6+):

import asyncio

async def main():
    print("Hello from legacy async world!")

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main())
    finally:
        loop.

**The "Running Loop" Gotcha:**
In older versions (and sometimes in specific environments like Jupyter Notebooks or test frameworks like `pytest-asyncio`), an event loop might already be running. Calling `asyncio.run()` or `loop.run_until_complete()` on a running loop raises a `RuntimeError: This event loop is already running`.

In those specific contexts, you don't "run" the loop; you **schedule** the coroutine as a Task:

```python
# Only use this if you are INSIDE an already running loop (e.g., Jupyter, FastAPI startup event)
asyncio.create_task(main())  # Python 3.7+
# or
asyncio.ensure_future(main()) # Older versions

Note: create_task/ensure_future schedules the coroutine but does not block the current code to wait for it. If this is your script's main entry point, the script will exit immediately unless you await the task or keep the loop alive.

Common Scenarios That Trigger the Warning

Beyond the simple "forgot asyncio.run()" mistake, this warning appears in several subtle situations.

1. Forgetting await Inside Another Coroutine

This is the most frequent logic error. You are already inside an async def, you call another async def, but you forget the await keyword.

async def fetch_data():
    return 42

async def main():
    # Missing await!
    data = fetch_data() 
    print(data) # Prints: 

asyncio.run(main())

Here, data becomes a coroutine object, not the integer 42. The warning fires when main finishes and fetch_data is garbage collected unawaited Not complicated — just consistent. Less friction, more output..

2. Using Async Functions in Synchronous Callbacks

You cannot await inside a regular def function. If you try to call an async function from a synchronous callback (like a requests hook, a Flask route without async support, or a threading.Thread target), you will get this warning because there is no event loop driving it Simple, but easy to overlook. Less friction, more output..

Wrong:

import threading

async def background_task():
    await asyncio.sleep(1)
    print("Done")

def sync_callback():
    background_task() # Warning! No loop here.

threading.Thread(target=sync_callback).start()

Fix: You must either move the logic to a synchronous function, run a dedicated loop in that thread (asyncio.run() inside the thread target), or use a thread-safe bridge like asyncio.run_coroutine_threadsafe() if a main loop exists elsewhere Worth keeping that in mind..

3. Discarding the Return Value of create_task

While asyncio.create_task(coro) does schedule the coroutine (preventing the warning if the loop runs long enough), if you create a task and immediately lose the reference to it and the loop stops, the task might be cancelled or garbage collected before finishing Easy to understand, harder to ignore. Practical, not theoretical..

async def main():
    asyncio.create_task(long_running_process()) # Fire and forget
    # main finishes immediately -> loop closes -> task destroyed -> Warning/Error

If you "fire and forget," ensure the task is stored somewhere (e.g., a global list) or that the application lifecycle keeps the loop running (like a web server) And that's really what it comes down to..

Deep Dive: What "Never Awaited" Actually Means Technically

When the Python interpreter prints RuntimeWarning: coroutine 'name' was never awaited, it is emitted from the coroutine object's __del__ method (destructor) That's the whole idea..

  1. Creation: coro = async_func() creates a coroutine object. Its internal state is CORO_CREATED.
  2. Execution: When await coro or loop.run_until_complete(coro) happens, the state moves to CORO_RUNNING and eventually CORO_CLOSED.
  3. Destruction: If the reference count drops to zero while the state is still CORO_CREATED (or CORO_RUNNING but suspended), the garbage collector calls __del__.
  4. Warning: __del__ checks the state. If it isn't CORO_CLOSED, it emits the RuntimeWarning.

This mechanism exists because an unawaited coroutine often represents a logic bug (forgotten await) or a resource leak (an open database connection, a file handle, or a network socket created in the first few lines of

More to Read

Recently Completed

Parallel Topics

Keep the Momentum

Thank you for reading about Runtimewarning Coroutine Main Was Never Awaited. We hope the information has been useful. Feel free to contact us if you have any questions. See you next time — don't forget to bookmark!
⌂ Back to Home