Skip to content

How to Run Python Async Functions with asyncio.run() and await

Python async code execution flow

The Problem

I wrote my first async function in Python. It looked simple:

my_async.py
async def fetch_data():
await asyncio.sleep(1)
return "data fetched"

Then I tried to run it:

main.py
result = fetch_data()
print(result)

I got this:

Output
<coroutine object fetch_data at 0x7f8b2c1d3e80>

Not the result I expected. The function didn’t execute. It just returned a coroutine object.

This is the most common confusion for Python developers learning async. You define async def, but the function doesn’t run when you call it directly.

Why This Happens

Async functions in Python are coroutines. When you call an async function directly (without await or asyncio.run()), you get a coroutine object, not the result.

Async Execution Flow
Synchronous Code
|
v
asyncio.run() <-- Entry point (creates event loop)
|
v
async function
|
v
await <-- Suspends execution, returns control to event loop
|
v
Result

The async function needs an event loop to run. Calling it directly doesn’t create one.

The Solution

Use asyncio.run() as your entry point from synchronous code:

correct_usage.py
import asyncio
async def fetch_data():
await asyncio.sleep(1) # simulates IO operation
return "data fetched"
# Entry point - run from synchronous code
result = asyncio.run(fetch_data())
print(result) # "data fetched"

asyncio.run() does three things:

  1. Creates a new event loop
  2. Runs your coroutine until it completes
  3. Closes the event loop

This is the recommended way to run async code from synchronous Python since Python 3.7.

Nested Async Calls

When calling async functions inside another async function, use await:

nested_async.py
import asyncio
async def fetch_data():
await asyncio.sleep(1)
return "data fetched"
async def process():
data = await fetch_data() # Must use await
return data.upper()
# Entry point
asyncio.run(process())

Without await, the inner coroutine is created but never executed:

wrong_nested.py
async def process_wrong():
data = fetch_data() # WRONG: creates coroutine, doesn't run it
# data is a coroutine object, not "data fetched"
return data.upper() # This will fail

Using the asyncio REPL

Python 3.8+ includes an asyncio REPL for experimentation:

terminal
python -m asyncio

Inside the REPL, you can use await directly:

asyncio REPL session
>>> import asyncio
>>> await asyncio.sleep(1, result='hello')
'hello'
>>> async def test():
... return 42
>>> await test()
42

This is useful for learning and debugging async code without needing to wrap everything in asyncio.run().

Common Mistakes

Mistake #1: Calling async function directly

mistake1.py
async def my_func():
return "hello"
result = my_func() # WRONG
# result is coroutine object, not "hello"

Fix:

fix1.py
result = asyncio.run(my_func()) # CORRECT

Mistake #2: Using asyncio.run() inside an async context

mistake2.py
async def outer():
async def inner():
return "hello"
return asyncio.run(inner()) # WRONG: RuntimeError

You can’t nest asyncio.run() calls. Each call creates a new event loop, but you’re already inside one.

Fix:

fix2.py
async def outer():
async def inner():
return "hello"
return await inner() # CORRECT: use await instead

Mistake #3: Blocking the event loop

mistake3.py
async def bad_example():
time.sleep(10) # WRONG: blocks the entire event loop
return "done"

time.sleep() blocks the event loop. All other async tasks stop running while this sleeps.

Fix:

fix3.py
async def good_example():
await asyncio.sleep(10) # CORRECT: non-blocking sleep
return "done"

When to Use Async

asyncio excels at IO-bound operations:

Good Use Cases
- HTTP requests (aiohttp, httpx)
- Database queries (asyncpg, databases)
- File operations (aiofiles)
- Network sockets
- Multiple concurrent tasks
Poor Use Cases:
- CPU-intensive calculations
- Local synchronous file operations
- Simple one-off operations

For IO-bound work, async lets you do multiple operations concurrently without blocking:

concurrent.py
import asyncio
import aiohttp
async def fetch_url(session, url):
response = await session.get(url)
return await response.text()
async def fetch_all(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch_url(session, url) for url in urls]
return await asyncio.gather(*tasks) # Run all concurrently
asyncio.run(fetch_all(['http://example.com', 'http://example.org']))

All URLs are fetched concurrently, not sequentially. This is where async shines.

Summary

To run Python async functions:

  1. Define with async def
  2. Call from sync code using asyncio.run(my_func())
  3. Call from async code using await my_func()
  4. Don’t block the event loop with synchronous operations

The pattern is simple: one entry point (asyncio.run()) and await everywhere else.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments