Python
async programming
non-blocking
synchronous functions
concurrency

Python run non-blocking async function from sync function

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Running an async function from synchronous Python code is easy if you are willing to block. Running it without blocking is a different problem. The key distinction is whether the synchronous caller wants the result immediately or just wants to schedule async work and continue.

If you call asyncio.run, the synchronous function blocks until the coroutine finishes. If you truly need non-blocking behavior from sync code, you usually need an already running event loop, often on another thread, and then submit work to that loop.

Blocking Option: asyncio.run

The simplest bridge from sync code to async code is asyncio.run:

python
1import asyncio
2
3async def fetch_value():
4    await asyncio.sleep(1)
5    return 42
6
7def main():
8    result = asyncio.run(fetch_value())
9    print(result)
10
11main()

This is correct for top-level entry points such as scripts or CLIs. But it is not non-blocking. The sync function waits until the coroutine is done.

What Non-Blocking Really Means Here

A synchronous function cannot both run a coroutine and keep using the same thread freely unless some event loop is already doing the asynchronous work elsewhere. So the usual non-blocking pattern is:

  • run an event loop in a background thread
  • submit coroutines to that loop
  • let the sync caller continue immediately

That gives the synchronous code fire-and-forget or future-based behavior.

Run the Event Loop in a Background Thread

Here is a working example using asyncio.run_coroutine_threadsafe:

python
1import asyncio
2import threading
3import time
4
5loop = asyncio.new_event_loop()
6
7def loop_worker():
8    asyncio.set_event_loop(loop)
9    loop.run_forever()
10
11thread = threading.Thread(target=loop_worker, daemon=True)
12thread.start()
13
14async def background_job(name: str):
15    await asyncio.sleep(2)
16    print(f"finished {name}")
17    return name.upper()
18
19def schedule_job(name: str):
20    future = asyncio.run_coroutine_threadsafe(background_job(name), loop)
21    return future
22
23future = schedule_job("alpha")
24print("sync code continues immediately")
25time.sleep(0.5)
26print("done waiting in sync caller for now")
27print("result later:", future.result())
28
29loop.call_soon_threadsafe(loop.stop)
30thread.join()

The important part is that schedule_job returns immediately after submitting the coroutine. The async work happens on the loop thread.

Fire-and-Forget Versus Waiting for the Result

Once you schedule async work from sync code, you must decide whether the caller will ever wait for the outcome.

If the sync code just wants to trigger background work, it can ignore the returned future. If it may need the result later, keep the future and inspect it when appropriate. That still keeps the initial call non-blocking.

What you should not do is call future.result() immediately if your goal was to avoid blocking. That simply moves the wait to a later line.

Avoid Nesting asyncio.run

A common error is trying to call asyncio.run from code that already lives inside an environment with an active event loop. That fails in notebooks, some web frameworks, and GUI applications.

If an event loop already exists, the better answer is usually:

  • if you are already in async code, use await or asyncio.create_task
  • if you are in sync code outside that loop, submit work to the existing loop thread-safely

The right solution depends on where the loop actually lives.

When asyncio.create_task Is the Right Tool

asyncio.create_task is non-blocking, but it can only be called from code already running inside the event loop thread:

python
1async def parent():
2    task = asyncio.create_task(background_job("beta"))
3    print("parent continues")
4    await task

This is useful to understand because many people ask for "run async from sync non-blocking" when the real problem is that they already have async code and should use task scheduling there instead.

Common Pitfalls

  • Calling asyncio.run and expecting non-blocking behavior.
  • Creating a coroutine object in sync code and never actually scheduling it.
  • Calling future.result() immediately after scheduling and then wondering why the sync caller still blocks.
  • Trying to use asyncio.create_task from a thread that does not own the running event loop.

Summary

  • 'asyncio.run is the simple sync-to-async bridge, but it blocks.'
  • Truly non-blocking submission from sync code usually requires an event loop running elsewhere, often in another thread.
  • Use asyncio.run_coroutine_threadsafe to submit work to that loop and continue immediately.
  • Use asyncio.create_task only when you are already inside async code on the loop thread.
  • Be explicit about whether the caller needs fire-and-forget behavior or a future result later.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.