Trio is a new async concurrency library for Python that's obsessed with correctness and usability.

On the correctness side, one of Trio's unique features is that it never discards exceptions: if you don't catch an exception, then eventually it will propagate out the top of your program and print a traceback to help you debug, just like in regular Python. Errors should never pass silently!

But... in Trio v0.6.0 and earlier, these tracebacks also contained a lot of clutter showing how the exception moved through Trio's internal plumbing, which made it difficult to see the parts that were relevant to your code. It's a small thing, but when you're debugging some nasty concurrency bug, it can make a big difference to have exactly the information you need, clearly laid out, without distractions.

And thanks to some hard work by John Belmonte, the just-released Trio v0.7.0 gives you exactly that: clean tracebacks, focused on your code, without the clutter. See below for some before/after comparisons.

Before Trio, I never really thought about where tracebacks came from, and I certainly never changed how I wrote code because I wanted it to produce a different traceback. Making useful tracebacks is the interpreter's job, right? In the process, we had to study how the interpreter manages tracebacks, how they interact with context managers, how to introspect stack usage in third-party libraries, and other arcane details ... but the results are totally worth it.

To me, this is what makes Trio so fun to work on: our goal is to make Python concurrency an order of magnitude friendlier and more accessible than it's ever been before, and that means we're constantly exploring new design spaces, discovering new things, and figuring out new ways to push the limits of the language.

If that sounds like fun to you too, then we're always looking for contributors. And don't worry, you don't need to be an expert on tracebacks or concurrency – the great thing about inventing something new is that we get to figure it out together!

Or, just scroll down to check out our new tracebacks. They're so pretty! 🤩

Simple example

Here's the simplest possible crashing Trio program:

import trio async def main (): raise RuntimeError ( "whoops" ) trio . run ( main )

With previous Trio versions, this code gave us a traceback like:

Traceback (most recent call last): File "error-example.py" , line 6 , in <module> trio . run ( main ) File ".../site-packages/trio/_core/_run.py" , line 1277 , in run return result . unwrap () File ".../site-packages/outcome/_sync.py" , line 107 , in unwrap raise self . error File ".../site-packages/trio/_core/_run.py" , line 1387 , in run_impl msg = task . context . run ( task . coro . send , next_send ) File ".../site-packages/contextvars/__init__.py" , line 38 , in run return callable ( * args , ** kwargs ) File ".../site-packages/trio/_core/_run.py" , line 970 , in init self . entry_queue . spawn () File ".../site-packages/async_generator/_util.py" , line 42 , in __aexit__ await self . _agen . asend ( None ) File ".../site-packages/async_generator/_impl.py" , line 366 , in step return await ANextIter ( self . _it , start_fn , * args ) File ".../site-packages/async_generator/_impl.py" , line 202 , in send return self . _invoke ( self . _it . send , value ) File ".../site-packages/async_generator/_impl.py" , line 209 , in _invoke result = fn ( * args ) File ".../site-packages/trio/_core/_run.py" , line 317 , in open_nursery await nursery . _nested_child_finished ( nested_child_exc ) File "/usr/lib/python3.6/contextlib.py" , line 99 , in __exit__ self . gen . throw ( type , value , traceback ) File ".../site-packages/trio/_core/_run.py" , line 202 , in open_cancel_scope yield scope File ".../site-packages/trio/_core/_multierror.py" , line 144 , in __exit__ raise filtered_exc File ".../site-packages/trio/_core/_run.py" , line 202 , in open_cancel_scope yield scope File ".../site-packages/trio/_core/_run.py" , line 317 , in open_nursery await nursery . _nested_child_finished ( nested_child_exc ) File ".../site-packages/trio/_core/_run.py" , line 428 , in _nested_child_finished raise MultiError ( self . _pending_excs ) File ".../site-packages/trio/_core/_run.py" , line 1387 , in run_impl msg = task . context . run ( task . coro . send , next_send ) File ".../site-packages/contextvars/__init__.py" , line 38 , in run return callable ( * args , ** kwargs ) File "error-example.py" , line 4 , in main raise RuntimeError ( "whoops" ) RuntimeError : whoops

It's accurate, and I guess it shows off how hard Trio is working on your behalf, but that's about all I can say for it – all the stuff our users care about is drowned in the noise.

But thanks to John's fixes, Trio v0.7.0 instead prints:

Traceback (most recent call last): File "error-example.py" , line 6 , in <module> trio . run ( main ) File ".../site-packages/trio/trio/_core/_run.py" , line 1328 , in run raise runner . main_task_outcome . error File "error-example.py" , line 4 , in main raise RuntimeError ( "whoops" ) RuntimeError : whoops

Three frames, straight to the point. We've removed almost all of Trio's internals from the traceback. And, for the one line that we can't remove (due to Python interpreter limitations), we've rewritten it so you can get a rough idea of what it's doing even when it's presented out of context like this. ( run re-raises the main task's error.)

A more complex example

Here's a program that starts two concurrent tasks, which both raise exceptions simultaneously. (If you're wondering what this "nursery" thing is, see this earlier post.)

import trio async def crasher1 (): raise KeyError async def crasher2 (): raise ValueError async def main (): async with trio . open_nursery () as nursery : nursery . start_soon ( crasher1 ) nursery . start_soon ( crasher2 ) trio . run ( main )

Hope your scroll wheel is ready, because here's what old versions of Trio printed for this:

Traceback (most recent call last): File "error-example.py" , line 14 , in <module> trio . run ( main ) File ".../site-packages/trio/_core/_run.py" , line 1277 , in run return result . unwrap () File ".../site-packages/outcome/_sync.py" , line 107 , in unwrap raise self . error File ".../site-packages/trio/_core/_run.py" , line 1387 , in run_impl msg = task . context . run ( task . coro . send , next_send ) File ".../site-packages/contextvars/__init__.py" , line 38 , in run return callable ( * args , ** kwargs ) File ".../site-packages/trio/_core/_run.py" , line 970 , in init self . entry_queue . spawn () File ".../site-packages/async_generator/_util.py" , line 42 , in __aexit__ await self . _agen . asend ( None ) File ".../site-packages/async_generator/_impl.py" , line 366 , in step return await ANextIter ( self . _it , start_fn , * args ) File ".../site-packages/async_generator/_impl.py" , line 202 , in send return self . _invoke ( self . _it . send , value ) File ".../site-packages/async_generator/_impl.py" , line 209 , in _invoke result = fn ( * args ) File ".../site-packages/trio/_core/_run.py" , line 317 , in open_nursery await nursery . _nested_child_finished ( nested_child_exc ) File "/usr/lib/python3.6/contextlib.py" , line 99 , in __exit__ self . gen . throw ( type , value , traceback ) File ".../site-packages/trio/_core/_run.py" , line 202 , in open_cancel_scope yield scope File ".../site-packages/trio/_core/_multierror.py" , line 144 , in __exit__ raise filtered_exc File ".../site-packages/trio/_core/_run.py" , line 1387 , in run_impl msg = task . context . run ( task . coro . send , next_send ) File ".../site-packages/contextvars/__init__.py" , line 38 , in run return callable ( * args , ** kwargs ) File "error-example.py" , line 12 , in main nursery . start_soon ( crasher2 ) File ".../site-packages/async_generator/_util.py" , line 42 , in __aexit__ await self . _agen . asend ( None ) File ".../site-packages/async_generator/_impl.py" , line 366 , in step return await ANextIter ( self . _it , start_fn , * args ) File ".../site-packages/async_generator/_impl.py" , line 202 , in send return self . _invoke ( self . _it . send , value ) File ".../site-packages/async_generator/_impl.py" , line 209 , in _invoke result = fn ( * args ) File ".../site-packages/trio/_core/_run.py" , line 317 , in open_nursery await nursery . _nested_child_finished ( nested_child_exc ) File "/usr/lib/python3.6/contextlib.py" , line 99 , in __exit__ self . gen . throw ( type , value , traceback ) File ".../site-packages/trio/_core/_run.py" , line 202 , in open_cancel_scope yield scope File ".../site-packages/trio/_core/_multierror.py" , line 144 , in __exit__ raise filtered_exc trio.MultiError : KeyError(), ValueError() Details of embedded exception 1: Traceback (most recent call last): File ".../site-packages/trio/_core/_run.py", line 202, in open_cancel_scope yield scope File ".../site-packages/trio/_core/_run.py", line 317, in open_nursery await nursery._nested_child_finished(nested_child_exc) File ".../site-packages/trio/_core/_run.py", line 428, in _nested_child_finished raise MultiError(self._pending_excs) File ".../site-packages/trio/_core/_run.py", line 1387, in run_impl msg = task.context.run(task.coro.send, next_send) File ".../site-packages/contextvars/__init__.py", line 38, in run return callable(*args, **kwargs) File "error-example.py", line 4, in crasher1 raise KeyError KeyError Details of embedded exception 2: Traceback (most recent call last): File ".../site-packages/trio/_core/_run.py", line 202, in open_cancel_scope yield scope File ".../site-packages/trio/_core/_run.py", line 317, in open_nursery await nursery._nested_child_finished(nested_child_exc) File ".../site-packages/trio/_core/_run.py", line 428, in _nested_child_finished raise MultiError(self._pending_excs) File ".../site-packages/trio/_core/_run.py", line 1387, in run_impl msg = task.context.run(task.coro.send, next_send) File ".../site-packages/contextvars/__init__.py", line 38, in run return callable(*args, **kwargs) File "error-example.py", line 7, in crasher2 raise ValueError ValueError

Accurate, but unreadable. But now, after rewriting substantial portions of Trio's core task management code, we get:

Traceback (most recent call last): File "error-example.py" , line 14 , in <module> trio . run ( main ) File ".../site-packages/trio/trio/_core/_run.py" , line 1328 , in run raise runner . main_task_outcome . error File "error-example.py" , line 12 , in main nursery . start_soon ( crasher2 ) File ".../site-packages/trio/trio/_core/_run.py" , line 395 , in __aexit__ raise combined_error_from_nursery trio.MultiError : KeyError(), ValueError() Details of embedded exception 1: Traceback (most recent call last): File "error-example.py", line 4, in crasher1 raise KeyError KeyError Details of embedded exception 2: Traceback (most recent call last): File "error-example.py", line 7, in crasher2 raise ValueError ValueError

Reading from the bottom up, the two exceptions each started in their respective tasks, then met and got bundled together into a MultiError , which propagated into the main task's nursery block, and then eventually up out of the call to trio.run .

Now when things go wrong, Trio shows you what you need to reconstruct what happened, and nothing else.

Comments

You can discuss this post on the Trio forum.