Ever used asyncio and wished you hadn't? A tiny (~400 lines) event loop for Python.
Python
739
79 commits
updated Apr 20, 2026
Ever used asyncio and wished you hadn't?
tinyio is a dead-simple event loop for Python, born out of my frustration with trying to get robust error handling with asyncio. (I'm not the only one running into its sharp corners: link1, link2.)
This is an alternative for the simple use-cases, where you just need an event loop, and want to crash the whole thing if anything goes wrong. (Raising an exception in every coroutine so it can clean up its resources.)
import tinyio
def slow_add_one(x: int):
yield tinyio.sleep(1)
return x + 1
def foo():
four, five = yield [slow_add_one(3), slow_add_one(4)]
return four, five
loop = tinyio.Loop()
out = loop.run(foo())
assert out == (4, 5)
yield rather than await, but the behaviour is the same. Await another coroutine with yield coro. Await on multiple with yield [coro1, coro2, ...] (a 'gather' in asyncio terminology; a 'nursery' in trio terminology).tinyio loops inside each other, none of this one-per-thread business.tinyio.Loop
tinyio.CancelledError
tinyio.sleep
tinyio.run_in_thread
pip install tinyio
Available at https://docs.kidger.site/tinyio.
yield — why not await like is normally seen for coroutines?The reason is that await does not offer a suspension point to an event loop (it just calls __await__ and maybe that offers a suspension point), so if we wanted to use that syntax then we'd need to replace yield coro with something like await tinyio.Task(coro). The traditional syntax is not worth the extra class.
yield statements, so it is just a normal function?You can distinguish it from a normal Python function by putting if False: yield somewhere inside its body. Another common trick is to put a yield statement after the final return statement. Bit ugly but oh well.
asyncio or trio?I wasted a lot of time trying to get correct error propagation with asyncio, trying to reason whether my tasks would be cleaned up correctly or not (edge-triggered vs level-triggered etc etc). trio is excellent but still has a one-loop-per-thread rule, and doesn't propagate cancellations to/from threads. These points inspired me to try writing my own.
tinyio has the following unique features, and as such may be the right choice if any of the following are must-haves for you:
Conversely, at least right now we don't ship batteries-included with a few things:
run_in_thread instead);If none of the bullet points are must-haves for you, or if any of its limitations are dealbreakers, then either trio or asyncio are likely to be better choices. :)
61,659 followers · starred Jul 2025
2,125 followers · starred Jul 2025
17,346 followers · starred Jul 2025
7,683 followers · starred Jul 2025
Ever used asyncio and wished you hadn't? A tiny (~400 lines) event loop for Python.
Python
739
79 commits
updated Apr 20, 2026
Ever used asyncio and wished you hadn't?
tinyio is a dead-simple event loop for Python, born out of my frustration with trying to get robust error handling with asyncio. (I'm not the only one running into its sharp corners: link1, link2.)
This is an alternative for the simple use-cases, where you just need an event loop, and want to crash the whole thing if anything goes wrong. (Raising an exception in every coroutine so it can clean up its resources.)
import tinyio
def slow_add_one(x: int):
yield tinyio.sleep(1)
return x + 1
def foo():
four, five = yield [slow_add_one(3), slow_add_one(4)]
return four, five
loop = tinyio.Loop()
out = loop.run(foo())
assert out == (4, 5)
yield rather than await, but the behaviour is the same. Await another coroutine with yield coro. Await on multiple with yield [coro1, coro2, ...] (a 'gather' in asyncio terminology; a 'nursery' in trio terminology).tinyio loops inside each other, none of this one-per-thread business.tinyio.Loop
tinyio.CancelledError
tinyio.sleep
tinyio.run_in_thread
pip install tinyio
Available at https://docs.kidger.site/tinyio.
yield — why not await like is normally seen for coroutines?The reason is that await does not offer a suspension point to an event loop (it just calls __await__ and maybe that offers a suspension point), so if we wanted to use that syntax then we'd need to replace yield coro with something like await tinyio.Task(coro). The traditional syntax is not worth the extra class.
yield statements, so it is just a normal function?You can distinguish it from a normal Python function by putting if False: yield somewhere inside its body. Another common trick is to put a yield statement after the final return statement. Bit ugly but oh well.
asyncio or trio?I wasted a lot of time trying to get correct error propagation with asyncio, trying to reason whether my tasks would be cleaned up correctly or not (edge-triggered vs level-triggered etc etc). trio is excellent but still has a one-loop-per-thread rule, and doesn't propagate cancellations to/from threads. These points inspired me to try writing my own.
tinyio has the following unique features, and as such may be the right choice if any of the following are must-haves for you:
Conversely, at least right now we don't ship batteries-included with a few things:
run_in_thread instead);If none of the bullet points are must-haves for you, or if any of its limitations are dealbreakers, then either trio or asyncio are likely to be better choices. :)
61,659 followers · starred Jul 2025
2,125 followers · starred Jul 2025
17,346 followers · starred Jul 2025
7,683 followers · starred Jul 2025