Announcing asyncio-thread-runner: you can have a little async (as a treat)
August 2025 ∙ two minute read ∙
Hi there!
Back in 2023, we made a thing for running async code from sync code.
I'm happy to announce that you can now install it from PyPI, and read the documented, tested, type-annotated code on GitHub! ⭐️
Without further ado:
asyncio-thread-runner allows you to run async code from sync code.
This is useful when you're doing some sync stuff, but:
- you also need to do some async stuff, without making everything async
- maybe the sync stuff is an existing application
- maybe you still want to use your favorite sync library
- or maybe you need just a little async, without having to pay the full price
Features:
- unlike asyncio.run(), it provides a long-lived event loop
- unlike asyncio.Runner, you can use it from multiple threads
- it allows you to use async context managers and iterables from sync code
- check out this article for why these are useful
Usage:
$ pip install asyncio-thread-runner
>>> async def double(i):
... return i * 2
...
>>> from asyncio_thread_runner import ThreadRunner
>>> runner = ThreadRunner()
>>> runner.run(double(2))
4
Annotated example:
import aiohttp
from asyncio_thread_runner import ThreadRunner
# you can use ThreadRunner as a context manager,
# or call runner.close() when you're done with it
with ThreadRunner() as runner:
# aiohttp.ClientSession() should be used as an async context manager,
# enter_context() will exit the context on runner shutdown;
# because instantiating ClientSession requires a running event loop,
# we pass it as a factory instead of calling it in the main thread
session = runner.enter_context(aiohttp.ClientSession)
# session.get() returns an async context manager...
request = session.get('https://death.andgravity.com/asyncio-bridge')
# which we turn into a normal one with wrap_context()
with runner.wrap_context(request) as response:
# response.content is an async iterator;
# we turn it into a normal iterator with wrap_iter()
lines = list(runner.wrap_iter(response.content))
# "got 935 lines"
print('got', len(lines), 'lines')
That's it for now.
Learned something new today? Share it with others, it really helps!