# tinyio, a four hundred line event loop that crashes the whole thing on error

> patrick-kidger/tinyio is a tiny event loop for Python written because the author wanted predictable error propagation from asyncio. It uses yield instead of await, cancels every coroutine when one fails, chains tracebacks along dependency chains, and propagates errors to and from threads, at the cost of shipping almost nothing else.

**patrick-kidger/tinyio** — Ever used asyncio and wished you hadn't? A tiny (~400 lines) event loop for Python.

- Repository: https://github.com/patrick-kidger/tinyio
- Stars: 739 · Forks: 24
- Language: Python
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/patrick-kidger-tinyio

## What tinyio is and the problem it was written for

tinyio is a dead-simple event loop for Python, and the README is blunt about the motivation. It was born out of the author's frustration with trying to get predictable error handling with asyncio, and it links two write-ups from other people hitting the same sharp corners.

The description of when to use it is narrow and specific: it 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. The parenthetical explains what that means. Raising an exception in every coroutine so it can clean up its resources.

That is the whole design thesis in two sentences. Most event loops are designed to keep running when part of the program fails, and the machinery that does that is exactly what makes it hard to reason about whether your tasks got cleaned up correctly. tinyio takes the opposite position. One coroutine raises, everything else is cancelled, and the exception propagates out of the loop.

The size is part of the pitch. The README calls it a tiny event loop of roughly 400 lines, and the point of a codebase that small is that it is hackable. You can read it, and you can change it, and you do not have to understand a scheduler internals to know what happens when an exception is raised.

The daily surface is four names. tinyio.Loop, tinyio.CancelledError, tinyio.sleep and tinyio.run_in_thread. There are no futures and no tasks to manage, which the README states plainly as a feature.

The package is Apache-2.0 licensed, built with hatchling, requires Python 3.11 or newer, and has no runtime dependencies at all. The project metadata classifies it as Development Status 3 - Alpha, which is the honest label for it. The releases are v0.2.1, v0.3.0 and v0.4.0 in January and March 2026, with the last push on 2026-04-20.

## Using it, and why the syntax is yield

The README's example is the whole API in about fifteen lines:

```python
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)
```

The unusual part, as the README says somewhat unusually, is that the syntax uses yield rather than await, though the behaviour is the same. Awaiting another coroutine is done with yield coro, and awaiting on several at once is yield with a list of them, which is a gather in asyncio terminology and a nursery in trio terminology.

The FAQ explains the reasoning for the choice. The reason is that await does not offer a suspension point to an event loop, since it just calls __await__ and maybe that offers a suspension point, so using that syntax would mean replacing yield coro with something like await tinyio.Task(coro). The traditional syntax is not worth the extra class.

That is a defensible trade and it has an observable consequence. Because coroutines are generator functions rather than awaitables, the standard tooling for them does not apply. The FAQ has a question on exactly this point: a function you want to be a coroutine but which has zero yield statements is just a normal function, and the answer is to put if False: yield somewhere inside its body, or to put a yield after the final return statement. The README calls that bit ugly but oh well, which is a fair description.

Installation is a single pip command:

```bash
pip install tinyio
```

The documentation lives at its own site, and the repository carries both a docs/ directory and a devdocs/ directory, with an mkdocs.yml at the top level and mkdocstrings configured for the API reference.

## Error handling, cancellation and threads

The feature list is what separates tinyio from the alternatives, and it has three parts.

The first is that an error in one coroutine cancels all coroutines across the entire event loop. Not the tasks in one nursery, the whole loop.

The second is the traceback chaining, and it is the detail that makes the first usable. If the erroring coroutine is sequentially depended on by a chain of other coroutines, then the tracebacks are chained for easier debugging. So if a chain of three coroutines calls the one that fails, you see all four tracebacks rather than only the innermost one, and the dependency structure is visible in the output.

The third is that errors propagate to and from synchronous operations run in threads, which is what run_in_thread is for.

The author's own account of the motivation is worth reading because it explains the design. The README says a lot of time was wasted trying to get correct error propagation with asyncio, trying to reason whether tasks would be cleaned up correctly or not, mentioning edge-triggered versus level-triggered. trio is called excellent but still has a one-loop-per-thread rule and does not propagate cancellations to or from threads. These points inspired the attempt to write something else.

The nesting property follows from the same design. tinyio loops can be nested inside each other, with no one-loop-per-thread business, which means a loop can be created inside a thread without the global registry problems that a single global loop would cause.

The trade is that you lose the parts of the ecosystem that come with a full framework. The README lists what does not ship: asynchronously launching subprocesses, making network requests, or accessing the file system. In all cases the answer is to use run_in_thread instead. It also does not support scheduling work on the event loop whilst cleaning up from errors.

That last limitation is worth pausing on, because it sounds like a description of a scheduler rather than a limitation. It means there is no built-in way to say start this piece of work and, if it fails, clean it up, which is the shape of a task group. The README is candid that if any of the missing items are dealbreakers then trio or asyncio are likely to be better choices.

## Project layout, tooling and the state of the work

The repository is small, which is what a four hundred line library looks like.

There is a tinyio/ directory holding the package itself, a tests/ directory, a docs/ directory for the published documentation and a devdocs/ directory for the development version of it, and an mkdocs.yml at the top level tying them together. There is a .pre-commit-config.yaml, a CONTRIBUTING.md, a LICENSE and a .github/ directory.

The pyproject.toml is more interesting than the size of the project would suggest, because the tooling is pinned exactly. The build backend is hatchling, and the package includes only the tinyio/ directory. The project has no dependencies at all, requires Python 3.11 or newer, and the version is 0.4.0.

The dependency groups show the shape of the project. The dev group pins prek at 0.3.9, pyright at 1.1.405, ruff at 0.13.0 and toml-sort at 0.23.1, which is a linting, type checking and formatting toolchain with every version pinned to a patch. The docs group is larger, with mkdocs, the mkdocs theme package, mkdocstrings and mkdocstrings-python for the API reference, griffe and hippogriffe for the introspection layer that mkdocstrings uses, pygments and pymdown-extensions for rendering, and mkdocs-include-exclude-files for controlling what the site includes.

The tests group lists pytest and trio. The presence of trio is worth noting: the author depends on trio for testing the comparison case, or at least uses it in the test suite, which is the sort of thing that makes the comparison in the README credible rather than rhetorical.

The hatch build configuration is also tuned for a library with a small public surface. reportFunctionMemberAccess is false, so inherited members do not show up in the generated docs, and reportIncompatibleMethodOverride and reportIncompatibleVariableOverride are both true, so overriding a method or a variable incompatibly is reported as an error rather than a warning.

The classification as Development Status 3 - Alpha is the honest summary. The releases are recent, the last push was on 2026-04-20, and the API is four names long, but alpha means the interface is still expected to move.

## How it fits against asyncio and trio

The README makes the comparison directly, and the shape of the argument is that you should probably not use tinyio.

The closing advice is that if none of the specific properties are must-haves for you, or if any of the limitations are dealbreakers, then either trio or asyncio are likely to be better choices. That is an unusual thing for a project README to say about itself, and it tells you what the author thinks of the tool's niche.

The must-haves are listed as four. Errors propagate to and from threads. There is no one-loop-per-thread rule. Error semantics are simple and predictable, in that the whole loop crashes if anything goes wrong. And the codebase is tiny and hackable.

Against asyncio, the argument is that the author could not get error propagation right and could not reason about whether tasks were cleaned up. That is a specific and credible complaint, because the two-task-group designs in asyncio have well known sharp edges around cancellation and exception groups, and the standard library has spent years adding tools to make them manageable. If your workload is a service that must not die when one request handler raises, asyncio's model is the right one and tinyio's is not.

Against trio, the argument is narrower. trio is described as excellent, and its structured concurrency model is genuinely a good answer to the cleanup question. What trio does not do, according to the README, is propagate cancellations to and from threads, and it keeps the one-loop-per-thread rule. So the choice between trio and tinyio is about whether thread integration and loop nesting matter more to you than the fact that tinyio is much smaller and has simpler cancellation semantics.

There is also a fourth option worth naming, which is not a library at all. If the goal is predictable error handling in concurrent code and you can change the shape of the problem, moving the concurrency to threads with a work queue gives you ordinary exception propagation with no event loop semantics to reason about at all, at the cost of the concurrency being per-thread rather than per-task.

The realistic use for tinyio is a script or a small service where you want to run several operations concurrently and you would rather the program died loudly than limped on with half its work cancelled in a way you have to reason about. In that setting the four-name API and the ability to read the whole implementation in an afternoon are the features that matter.

## Conclusion

Adopt tinyio if you need an event loop where a failure takes the program down cleanly rather than leaving tasks half cleaned up, and you are willing to write your own concurrency primitives on top of it. Skip it if you need subprocesses, network calls or filesystem access on the event loop itself, since none of those ship and the README says to use run_in_thread instead. Verify first by installing the package, running the README example unchanged, and then checking that an exception in one coroutine cancels the others as you expect.

## FAQ

### What is tinyio and how is it different from asyncio?

It is a roughly 400 line event loop for Python written because the author could not get correct error propagation and task cleanup with asyncio. Its distinctive properties are that an error in one coroutine cancels every coroutine in the loop, that tracebacks are chained along dependency chains, and that errors propagate to and from synchronous code running in threads.

### Why does tinyio use yield instead of await?

The README explains that await does not offer a suspension point to an event loop, since it only calls __await__ and maybe that offers one, so using await would mean writing await tinyio.Task(coro) instead of yield coro. The traditional syntax is not worth the extra class. A coroutine with no yield statements therefore needs if False: yield inside it.

### How do I install and start using tinyio?

With pip install tinyio, then create a tinyio.Loop and call run on a generator function that yields either a single coroutine or a list of them. The README's example awaits two slow_add_one coroutines with a list yield and asserts the result is a pair of values.

### What does tinyio not provide?

The README says it does not ship asynchronously launching subprocesses, making network requests or accessing the file system, and in all cases you should use run_in_thread instead. It also does not support scheduling work on the event loop whilst cleaning up from errors, so if any of these are dealbreakers the README suggests trio or asyncio instead.

### What licence is tinyio released under and what does it require?

Apache-2.0, with the licence held in the LICENSE file and the project metadata classifying it as Development Status 3 - Alpha. It is built with hatchling, requires Python 3.11 or newer, has no runtime dependencies, and the current version is 0.4.0.

## Sources

- [Issues](https://github.com/patrick-kidger/tinyio/issues)
- [License: Apache-2.0](https://github.com/patrick-kidger/tinyio/blob/main/LICENSE)
- [patrick-kidger/tinyio on GitHub](https://github.com/patrick-kidger/tinyio)
- [README](https://github.com/patrick-kidger/tinyio/blob/main/README.md)
- [Releases](https://github.com/patrick-kidger/tinyio/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/patrick-kidger-tinyio
