Trio: a friendly Python library for async concurrency and I/O

The Trio project’s goal is to produce a production-quality, permissively licensed, async/await-native I/O library for Python. Like all async libraries, its main purpose is to help you write programs that do multiple things at the same time with parallelized I/O. A web spider that wants to fetch lots of pages in parallel, a web server that needs to juggle lots of downloads and websocket connections at the same time, a process supervisor monitoring multiple subprocesses… that sort of thing. Compared to other libraries, Trio attempts to distinguish itself with an obsessive focus on usability and correctness. Concurrency is complicated; we try to make it easy to get things right.

Trio was built from the ground up to take advantage of the latest Python features, and draws inspiration from many sources, in particular Dave Beazley’s Curio. The resulting design is radically simpler than older competitors like asyncio and Twisted, yet just as capable. Trio is the Python I/O library I always wanted; I find it makes building I/O-oriented programs easier, less error-prone, and just plain more fun. Perhaps you’ll find the same.

This project is young and still somewhat experimental: the overall design is solid and the existing features are fully tested and documented, but you may encounter missing functionality or rough edges. We do encourage you do use it, but you should read and subscribe to issue #1 to get warning and a chance to give feedback about any compatibility-breaking changes.

Vital statistics:

Tutorial

Welcome to the Trio tutorial! Trio is a modern Python library for writing asynchronous applications – that is, programs that want to do multiple things at the same time with parallelized I/O, like a web spider that fetches lots of pages in parallel, a web server juggling lots of simultaneous downloads… that sort of thing. Here we’ll try to give a gentle introduction to asynchronous programming with Trio.

We assume that you’re familiar with Python in general, but don’t worry – we don’t assume you know anything about asynchronous programming or Python’s new async/await feature.

Also, unlike many async/await tutorials, we assume that your goal is to use Trio to write interesting programs, so we won’t go into the nitty-gritty details of how async/await is implemented inside the Python interpreter. The word “coroutine” is never mentioned. The fact is, you really don’t need to know any of that stuff unless you want to implement a library like Trio, so we leave it out (though we’ll throw in a few links for those who want to dig deeper).

Okay, ready? Let’s get started.

Before you begin

  1. Make sure you’re using Python 3.7 or newer.

  2. python3 -m pip install --upgrade trio (or on Windows, maybe py -3 -m pip install --upgrade triodetails)

  3. Can you import trio? If so then you’re good to go!

If you get lost or confused…

…then we want to know! We have a friendly chat channel, you can ask questions using the “python-trio” tag on StackOverflow, or just file a bug (if our documentation is confusing, that’s our fault, and we want to fix it!).

Async functions

Python 3.5 added a major new feature: async functions. Using Trio is all about writing async functions, so let’s start there.

An async function is defined like a normal function, except you write async def instead of def:

# A regular function
def regular_double(x):
    return 2 * x

# An async function
async def async_double(x):
    return 2 * x

“Async” is short for “asynchronous”; we’ll sometimes refer to regular functions like regular_double as “synchronous functions”, to distinguish them from async functions.

From a user’s point of view, there are two differences between an async function and a regular function:

  1. To call an async function, you have to use the await keyword. So instead of writing regular_double(3), you write await async_double(3).

  2. You can’t use the await keyword inside the body of a regular function. If you try it, you’ll get a syntax error:

    def print_double(x):
        print(await async_double(x))   # <-- SyntaxError here
    

    But inside an async function, await is allowed:

    async def print_double(x):
        print(await async_double(x))   # <-- OK!
    

Now, let’s think about the consequences here: if you need await to call an async function, and only async functions can use await… here’s a little table:

If a function like this

wants to call a function like this

is it gonna happen?

sync

sync

sync

async

NOPE

async

sync

async

async

So in summary: As a user, the entire advantage of async functions over regular functions is that async functions have a superpower: they can call other async functions.

This immediately raises two questions: how, and why? Specifically:

When your Python program starts up, it’s running regular old sync code. So there’s a chicken-and-the-egg problem: once we’re running an async function we can call other async functions, but how do we call that first async function?

And, if the only reason to write an async function is that it can call other async functions, why on earth would we ever use them in the first place? I mean, as superpowers go this seems a bit pointless. Wouldn’t it be simpler to just… not use any async functions at all?

This is where an async library like Trio comes in. It provides two things:

  1. A runner function, which is a special synchronous function that takes and calls an asynchronous function. In Trio, this is trio.run:

    import trio
    
    async def async_double(x):
        return 2 * x
    
    trio.run(async_double, 3)  # returns 6
    

    So that answers the “how” part.

  2. A bunch of useful async functions – in particular, functions for doing I/O. So that answers the “why”: these functions are async, and they’re useful, so if you want to use them, you have to write async code. If you think keeping track of these async and await things is annoying, then too bad – you’ve got no choice in the matter! (Well, OK, you could just not use Trio. That’s a legitimate option. But it turns out that the async/await stuff is actually a good thing, for reasons we’ll discuss a little bit later.)

    Here’s an example function that uses trio.sleep(). (trio.sleep() is like time.sleep(), but with more async.)

    import trio
    
    async def double_sleep(x):
        await trio.sleep(2 * x)
    
    trio.run(double_sleep, 3)  # does nothing for 6 seconds then returns
    

So it turns out our async_double function is actually a bad example. I mean, it works, it’s fine, there’s nothing wrong with it, but it’s pointless: it could just as easily be written as a regular function, and it would be more useful that way. double_sleep is a much more typical example: we have to make it async, because it calls another async function. The end result is a kind of async sandwich, with Trio on both sides and our code in the middle:

trio.run -> double_sleep -> trio.sleep

This “sandwich” structure is typical for async code; in general, it looks like:

trio.run -> [async function] -> ... -> [async function] -> trio.whatever

It’s exactly the functions on the path between trio.run() and trio.whatever that have to be async. Trio provides the async bread, and then your code makes up the async sandwich’s tasty async filling. Other functions (e.g., helpers you call along the way) should generally be regular, non-async functions.

Warning: don’t forget that await!

Now would be a good time to open up a Python prompt and experiment a little with writing simple async functions and running them with trio.run.

At some point in this process, you’ll probably write some code like this, that tries to call an async function but leaves out the await:

import time
import trio

async def broken_double_sleep(x):
    print("*yawn* Going to sleep")
    start_time = time.perf_counter()

    # Whoops, we forgot the 'await'!
    trio.sleep(2 * x)

    sleep_time = time.perf_counter() - start_time
    print(f"Woke up after {sleep_time:.2f} seconds, feeling well rested!")

trio.run(broken_double_sleep, 3)

You might think that Python would raise an error here, like it does for other kinds of mistakes we sometimes make when calling a function. Like, if we forgot to pass trio.sleep() its required argument, then we would get a nice TypeError saying so. But unfortunately, if you forget an await, you don’t get that. What you actually get is:

>>> trio.run(broken_double_sleep, 3)
*yawn* Going to sleep
Woke up after 0.00 seconds, feeling well rested!
__main__:4: RuntimeWarning: coroutine 'sleep' was never awaited
>>>

This is clearly broken – 0.00 seconds is not long enough to feel well rested! Yet the code acts like it succeeded – no exception was raised. The only clue that something went wrong is that it prints RuntimeWarning: coroutine 'sleep' was never awaited. Also, the exact place where the warning is printed might vary, because it depends on the whims of the garbage collector. If you’re using PyPy, you might not even get a warning at all until the next GC collection runs:

# On PyPy:
>>>> trio.run(broken_double_sleep, 3)
*yawn* Going to sleep
Woke up after 0.00 seconds, feeling well rested!
>>>> # what the ... ?? not even a warning!

>>>> # but forcing a garbage collection gives us a warning:
>>>> import gc
>>>> gc.collect()
/home/njs/pypy-3.8-nightly/lib-python/3/importlib/_bootstrap.py:191: RuntimeWarning: coroutine 'sleep' was never awaited
if _module_locks.get(name) is wr:    # XXX PyPy fix?
0
>>>>

(If you can’t see the warning above, try scrolling right.)

Forgetting an await like this is an incredibly common mistake. You will mess this up. Everyone does. And Python will not help you as much as you’d hope 😞. The key thing to remember is: if you see the magic words RuntimeWarning: coroutine '...' was never awaited, then this always means that you made the mistake of leaving out an await somewhere, and you should ignore all the other error messages you see and go fix that first, because there’s a good chance the other stuff is just collateral damage. I’m not even sure what all that other junk in the PyPy output is. Fortunately I don’t need to know, I just need to fix my function!

(“I thought you said you weren’t going to mention coroutines!” Yes, well, I didn’t mention coroutines, Python did. Take it up with Guido! But seriously, this is unfortunately a place where the internal implementation details do leak out a bit.)

Why does this happen? In Trio, every time we use await it’s to call an async function, and every time we call an async function we use await. But Python’s trying to keep its options open for other libraries that are ahem a little less organized about things. So while for our purposes we can think of await trio.sleep(...) as a single piece of syntax, Python thinks of it as two things: first a function call that returns this weird “coroutine” object:

>>> trio.sleep(3)
<coroutine object sleep at 0x7f5ac77be6d0>

and then that object gets passed to await, which actually runs the function. So if you forget await, then two bad things happen: your function doesn’t actually get called, and you get a “coroutine” object where you might have been expecting something else, like a number:

>>> async_double(3) + 1
TypeError: unsupported operand type(s) for +: 'coroutine' and 'int'

If you didn’t already mess this up naturally, then give it a try on purpose: try writing some code with a missing await, or an extra await, and see what you get. This way you’ll be prepared for when it happens to you for real.

And remember: watch out for RuntimeWarning: coroutine '...' was never awaited; it means you need to find and fix your missing await.

Okay, let’s see something cool already

So now we’ve started using Trio, but so far all we’ve learned to do is write functions that print things and sleep for various lengths of time. Interesting enough, but we could just as easily have done that with time.sleep(). async/await is useless!

Well, not really. Trio has one more trick up its sleeve, that makes async functions more powerful than regular functions: it can run multiple async functions at the same time. Here’s an example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# tasks-intro.py

import trio

async def child1():
    print("  child1: started! sleeping now...")
    await trio.sleep(1)
    print("  child1: exiting!")

async def child2():
    print("  child2: started! sleeping now...")
    await trio.sleep(1)
    print("  child2: exiting!")

async def parent():
    print("parent: started!")
    async with trio.open_nursery() as nursery:
        print("parent: spawning child1...")
        nursery.start_soon(child1)

        print("parent: spawning child2...")
        nursery.start_soon(child2)

        print("parent: waiting for children to finish...")
        # -- we exit the nursery block here --
    print("parent: all done!")

trio.run(parent)

There’s a lot going on in here, so we’ll take it one step at a time. In the first part, we define two async functions child1 and child2. These should look familiar from the last section:

 5
 6
 7
 8
 9
10
11
12
13
async def child1():
    print("  child1: started! sleeping now...")
    await trio.sleep(1)
    print("  child1: exiting!")

async def child2():
    print("  child2: started! sleeping now...")
    await trio.sleep(1)
    print("  child2: exiting!")

Next, we define parent as an async function that’s going to call child1 and child2 concurrently:

15
16
17
18
19
20
21
22
23
24
25
26
async def parent():
    print("parent: started!")
    async with trio.open_nursery() as nursery:
        print("parent: spawning child1...")
        nursery.start_soon(child1)

        print("parent: spawning child2...")
        nursery.start_soon(child2)

        print("parent: waiting for children to finish...")
        # -- we exit the nursery block here --
    print("parent: all done!")

It does this by using a mysterious async with statement to create a “nursery”, and then “spawns” child1 and child2 into the nursery.

Let’s start with this async with thing. It’s actually pretty simple. In regular Python, a statement like with someobj: ... instructs the interpreter to call someobj.__enter__() at the beginning of the block, and to call someobj.__exit__() at the end of the block. We call someobj a “context manager”. An async with does exactly the same thing, except that where a regular with statement calls regular methods, an async with statement calls async methods: at the start of the block it does await someobj.__aenter__() and at that end of the block it does await someobj.__aexit__(). In this case we call someobj an “async context manager”. So in short: with blocks are a shorthand for calling some functions, and since with async/await Python now has two kinds of functions, it also needs two kinds of with blocks. That’s all there is to it! If you understand async functions, then you understand async with.

Note

This example doesn’t use them, but while we’re here we might as well mention the one other piece of new syntax that async/await added: async for. It’s basically the same idea as async with versus with: An async for loop is just like a for loop, except that where a for loop does iterator.__next__() to fetch the next item, an async for does await async_iterator.__anext__(). Now you understand all of async/await. Basically just remember that it involves making sandwiches and sticking the word “async” in front of everything, and you’ll do fine.

Now that we understand async with, let’s look at parent again:

15
16
17
18
19
20
21
22
23
24
25
26
async def parent():
    print("parent: started!")
    async with trio.open_nursery() as nursery:
        print("parent: spawning child1...")
        nursery.start_soon(child1)

        print("parent: spawning child2...")
        nursery.start_soon(child2)

        print("parent: waiting for children to finish...")
        # -- we exit the nursery block here --
    print("parent: all done!")

There are only 4 lines of code that really do anything here. On line 17, we use trio.open_nursery() to get a “nursery” object, and then inside the async with block we call nursery.start_soon twice, on lines 19 and 22. There are actually two ways to call an async function: the first one is the one we already saw, using await async_fn(); the new one is nursery.start_soon(async_fn): it asks Trio to start running this async function, but then returns immediately without waiting for the function to finish. So after our two calls to nursery.start_soon, child1 and child2 are now running in the background. And then at line 25, the commented line, we hit the end of the async with block, and the nursery’s __aexit__ function runs. What this does is force parent to stop here and wait for all the children in the nursery to exit. This is why you have to use async with to get a nursery: it gives us a way to make sure that the child calls can’t run away and get lost. One reason this is important is that if there’s a bug or other problem in one of the children, and it raises an exception, then it lets us propagate that exception into the parent; in many other frameworks, exceptions like this are just discarded. Trio never discards exceptions.

Ok! Let’s try running it and see what we get:

parent: started!
parent: spawning child1...
parent: spawning child2...
parent: waiting for children to finish...
  child2: started! sleeping now...
  child1: started! sleeping now...
    [... 1 second passes ...]
  child1: exiting!
  child2: exiting!
parent: all done!

(Your output might have the order of the “started” and/or “exiting” lines swapped compared to mine.)

Notice that child1 and child2 both start together and then both exit together. And, even though we made two calls to trio.sleep(1), the program finished in just one second total. So it looks like child1 and child2 really are running at the same time!

Now, if you’re familiar with programming using threads, this might look familiar – and that’s intentional. But it’s important to realize that there are no threads here. All of this is happening in a single thread. To remind ourselves of this, we use slightly different terminology: instead of spawning two “threads”, we say that we spawned two “tasks”. There are two differences between tasks and threads: (1) many tasks can take turns running on a single thread, and (2) with threads, the Python interpreter/operating system can switch which thread is running whenever they feel like it; with tasks, we can only switch at certain designated places we call “checkpoints”. In the next section, we’ll dig into what this means.

Task switching illustrated

The big idea behind async/await-based libraries like Trio is to run lots of tasks simultaneously on a single thread by switching between them at appropriate places – so for example, if we’re implementing a web server, then one task could be sending an HTTP response at the same time as another task is waiting for new connections. If all you want to do is use Trio, then you don’t need to understand all the nitty-gritty detail of how this switching works – but it’s very useful to have at least a general intuition about what Trio is doing “under the hood” when your code is executing. To help build that intuition, let’s look more closely at how Trio ran our example from the last section.

Fortunately, Trio provides a rich set of tools for inspecting and debugging your programs. Here we want to watch trio.run() at work, which we can do by writing a class we’ll call Tracer, which implements Trio’s Instrument interface. Its job is to log various events as they happen:

class Tracer(trio.abc.Instrument):
    def before_run(self):
        print("!!! run started")

    def _print_with_task(self, msg, task):
        # repr(task) is perhaps more useful than task.name in general,
        # but in context of a tutorial the extra noise is unhelpful.
        print(f"{msg}: {task.name}")

    def task_spawned(self, task):
        self._print_with_task("### new task spawned", task)

    def task_scheduled(self, task):
        self._print_with_task("### task scheduled", task)

    def before_task_step(self, task):
        self._print_with_task(">>> about to run one step of task", task)

    def after_task_step(self, task):
        self._print_with_task("<<< task step finished", task)

    def task_exited(self, task):
        self._print_with_task("### task exited", task)

    def before_io_wait(self, timeout):
        if timeout:
            print(f"### waiting for I/O for up to {timeout} seconds")
        else:
            print("### doing a quick check for I/O")
        self._sleep_time = trio.current_time()

    def after_io_wait(self, timeout):
        duration = trio.current_time() - self._sleep_time
        print(f"### finished I/O check (took {duration} seconds)")

    def after_run(self):
        print("!!! run finished")

Then we re-run our example program from the previous section, but this time we pass trio.run() a Tracer object:

trio.run(parent, instruments=[Tracer()])

This generates a lot of output, so we’ll go through it one step at a time.

First, there’s a bit of chatter while Trio gets ready to run our code. Most of this is irrelevant to us for now, but in the middle you can see that Trio has created a task for the __main__.parent function, and “scheduled” it (i.e., made a note that it should be run soon):

$ python3 tutorial/tasks-with-trace.py
!!! run started
### new task spawned: <init>
### task scheduled: <init>
### doing a quick check for I/O
### finished I/O check (took 1.1122087016701698e-05 seconds)
>>> about to run one step of task: <init>
### new task spawned: <call soon task>
### task scheduled: <call soon task>
### new task spawned: __main__.parent
### task scheduled: __main__.parent
<<< task step finished: <init>
### doing a quick check for I/O
### finished I/O check (took 6.4980704337358475e-06 seconds)

Once the initial housekeeping is done, Trio starts running the parent function, and you can see parent creating the two child tasks. Then it hits the end of the async with block, and pauses:

>>> about to run one step of task: __main__.parent
parent: started!
parent: spawning child1...
### new task spawned: __main__.child1
### task scheduled: __main__.child1
parent: spawning child2...
### new task spawned: __main__.child2
### task scheduled: __main__.child2
parent: waiting for children to finish...
<<< task step finished: __main__.parent

Control then goes back to trio.run(), which logs a bit more internal chatter:

>>> about to run one step of task: <call soon task>
<<< task step finished: <call soon task>
### doing a quick check for I/O
### finished I/O check (took 5.476875230669975e-06 seconds)

And then gives the two child tasks a chance to run:

>>> about to run one step of task: __main__.child2
  child2 started! sleeping now...
<<< task step finished: __main__.child2

>>> about to run one step of task: __main__.child1
  child1: started! sleeping now...
<<< task step finished: __main__.child1

Each task runs until it hits the call to trio.sleep(), and then suddenly we’re back in trio.run() deciding what to run next. How does this happen? The secret is that trio.run() and trio.sleep() work together to make it happen: trio.sleep() has access to some special magic that lets it pause itself, so it sends a note to trio.run() requesting to be woken again after 1 second, and then suspends the task. And once the task is suspended, Python gives control back to trio.run(), which decides what to do next. (If this sounds similar to the way that generators can suspend execution by doing a yield, then that’s not a coincidence: inside the Python interpreter, there’s a lot of overlap between the implementation of generators and async functions.)

Note

You might wonder whether you can mix-and-match primitives from different async libraries. For example, could we use trio.run() together with asyncio.sleep()? The answer is no, we can’t, and the paragraph above explains why: the two sides of our async sandwich have a private language they use to talk to each other, and different libraries use different languages. So if you try to call asyncio.sleep() from inside a trio.run(), then Trio will get very confused indeed and probably blow up in some dramatic way.

Only async functions have access to the special magic for suspending a task, so only async functions can cause the program to switch to a different task. What this means is that if a call doesn’t have an await on it, then you know that it can’t be a place where your task will be suspended. This makes tasks much easier to reason about than threads, because there are far fewer ways that tasks can be interleaved with each other and stomp on each others’ state. (For example, in Trio a statement like a += 1 is always atomic – even if a is some arbitrarily complicated custom object!) Trio also makes some further guarantees beyond that, but that’s the big one.

And now you also know why parent had to use an async with to open the nursery: if we had used a regular with block, then it wouldn’t have been able to pause at the end and wait for the children to finish; we need our cleanup function to be async, which is exactly what async with gives us.

Now, back to our execution point. To recap: at this point parent is waiting on child1 and child2, and both children are sleeping. So trio.run() checks its notes, and sees that there’s nothing to be done until those sleeps finish – unless possibly some external I/O event comes in. If that happened, then it might give us something to do. Of course we aren’t doing any I/O here so it won’t happen, but in other situations it could. So next it calls an operating system primitive to put the whole process to sleep:

### waiting for I/O for up to 0.9999009938910604 seconds

And in fact no I/O does arrive, so one second later we wake up again, and Trio checks its notes again. At this point it checks the current time, compares it to the notes that trio.sleep() sent saying when the two child tasks should be woken up again, and realizes that they’ve slept for long enough, so it schedules them to run soon:

### finished I/O check (took 1.0006483688484877 seconds)
### task scheduled: __main__.child1
### task scheduled: __main__.child2

And then the children get to run, and this time they run to completion. Remember how parent is waiting for them to finish? Notice how parent gets scheduled when the first child exits:

>>> about to run one step of task: __main__.child1
  child1: exiting!
### task scheduled: __main__.parent
### task exited: __main__.child1
<<< task step finished: __main__.child1

>>> about to run one step of task: __main__.child2
  child2 exiting!
### task exited: __main__.child2
<<< task step finished: __main__.child2

Then, after another check for I/O, parent wakes up. The nursery cleanup code notices that all its children have exited, and lets the nursery block finish. And then parent makes a final print and exits:

### doing a quick check for I/O
### finished I/O check (took 9.045004844665527e-06 seconds)

>>> about to run one step of task: __main__.parent
parent: all done!
### task scheduled: <init>
### task exited: __main__.parent
<<< task step finished: __main__.parent

And finally, after a bit more internal bookkeeping, trio.run() exits too:

### doing a quick check for I/O
### finished I/O check (took 5.996786057949066e-06 seconds)
>>> about to run one step of task: <init>
### task scheduled: <call soon task>
### task scheduled: <init>
<<< task step finished: <init>
### doing a quick check for I/O
### finished I/O check (took 6.258022040128708e-06 seconds)
>>> about to run one step of task: <call soon task>
### task exited: <call soon task>
<<< task step finished: <call soon task>
>>> about to run one step of task: <init>
### task exited: <init>
<<< task step finished: <init>
!!! run finished

You made it!

That was a lot of text, but again, you don’t need to understand everything here to use Trio – in fact, Trio goes to great lengths to make each task feel like it executes in a simple, linear way. (Just like your operating system goes to great lengths to make it feel like your single-threaded code executes in a simple linear way, even though under the covers the operating system juggles between different threads and processes in essentially the same way Trio does.) But it is useful to have a rough model in your head of how the code you write is actually executed, and – most importantly – the consequences of that for parallelism.

Alternatively, if this has just whetted your appetite and you want to know more about how async/await works internally, then this blog post is a good deep dive, or check out this great walkthrough to see how to build a simple async I/O framework from the ground up.

A kinder, gentler GIL

Speaking of parallelism – let’s zoom out for a moment and talk about how async/await compares to other ways of handling concurrency in Python.

As we’ve already noted, Trio tasks are conceptually rather similar to Python’s built-in threads, as provided by the threading module. And in all common Python implementations, threads have a famous limitation: the Global Interpreter Lock, or “GIL” for short. The GIL means that even if you use multiple threads, your code still (mostly) ends up running on a single core. People tend to find this frustrating.

But from Trio’s point of view, the problem with the GIL isn’t that it restricts parallelism. Of course it would be nice if Python had better options for taking advantage of multiple cores, but that’s an extremely difficult problem to solve, and in the meantime there are lots of problems where a single core is totally adequate – or where if it isn’t, then process-level or machine-level parallelism works fine.

No, the problem with the GIL is that it’s a lousy deal: we give up on using multiple cores, and in exchange we get… almost all the same challenges and mind-bending bugs that come with real parallel programming, and – to add insult to injury – pretty poor scalability. Threads in Python just aren’t that appealing.

Trio doesn’t make your code run on multiple cores; in fact, as we saw above, it’s baked into Trio’s design that when it has multiple tasks, they take turns, so at each moment only one of them is actively running. We’re not so much overcoming the GIL as embracing it. But if you’re willing to accept that, plus a bit of extra work to put these new async and await keywords in the right places, then in exchange you get:

  • Excellent scalability: Trio can run 10,000+ tasks simultaneously without breaking a sweat, so long as their total CPU demands don’t exceed what a single core can provide. (This is common in, for example, network servers that have lots of clients connected, but only a few active at any given time.)

  • Fancy features: most threading systems are implemented in C and restricted to whatever features the operating system provides. In Trio our logic is all in Python, which makes it possible to implement powerful and ergonomic features like Trio’s cancellation system.

  • Code that’s easier to reason about: the await keyword means that potential task-switching points are explicitly marked within each function. This can make Trio code dramatically easier to reason about than the equivalent program using threads.

Certainly it’s not appropriate for every app… but there are a lot of situations where the trade-offs here look pretty appealing.

There is one downside that’s important to keep in mind, though. Making checkpoints explicit gives you more control over how your tasks can be interleaved – but with great power comes great responsibility. With threads, the runtime environment is responsible for making sure that each thread gets its fair share of running time. With Trio, if some task runs off and does stuff for seconds on end without executing a checkpoint, then… all your other tasks will just have to wait.

Here’s an example of how this can go wrong. Take our example from above, and replace the calls to trio.sleep() with calls to time.sleep(). If we run our modified program, we’ll see something like:

parent: started!
parent: spawning child1...
parent: spawning child2...
parent: waiting for children to finish...
  child2 started! sleeping now...
    [... pauses for 1 second ...]
  child2 exiting!
  child1: started! sleeping now...
    [... pauses for 1 second ...]
  child1: exiting!
parent: all done!

One of the major reasons why Trio has such a rich instrumentation API is to make it possible to write debugging tools to catch issues like this.

Networking with Trio

Now let’s take what we’ve learned and use it to do some I/O, which is where async/await really shines.

The traditional toy application for demonstrating network APIs is an “echo server”: a program that awaits arbitrary data from remote clients, and then sends that same data right back. (Probably a more relevant example these days would be an application that does lots of concurrent HTTP requests, but for that you need an HTTP library such as asks, so we’ll stick with the echo server tradition.)

In this tutorial, we present both ends of the pipe: the client, and the server. The client periodically sends data to the server, and displays its answers. The server awaits connections; when a client connects, it recopies the received data back on the pipe.

An echo client

To start with, here’s an example echo client, i.e., the program that will send some data at our echo server and get responses back:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# echo-client.py

import sys
import trio

# arbitrary, but:
# - must be in between 1024 and 65535
# - can't be in use by some other program on your computer
# - must match what we set in our echo server
PORT = 12345

async def sender(client_stream):
    print("sender: started!")
    while True:
        data = b"async can sometimes be confusing, but I believe in you!"
        print(f"sender: sending {data!r}")
        await client_stream.send_all(data)
        await trio.sleep(1)

async def receiver(client_stream):
    print("receiver: started!")
    async for data in client_stream:
        print(f"receiver: got data {data!r}")
    print("receiver: connection closed")
    sys.exit()

async def parent():
    print(f"parent: connecting to 127.0.0.1:{PORT}")
    client_stream = await trio.open_tcp_stream("127.0.0.1", PORT)
    async with client_stream:
        async with trio.open_nursery() as nursery:
            print("parent: spawning sender...")
            nursery.start_soon(sender, client_stream)

            print("parent: spawning receiver...")
            nursery.start_soon(receiver, client_stream)

trio.run(parent)

Note that this code will not work without a TCP server such as the one we’ll implement below.

The overall structure here should be familiar, because it’s just like our last example: we have a parent task, which spawns two child tasks to do the actual work, and then at the end of the async with block it switches into full-time parenting mode while waiting for them to finish. But now instead of just calling trio.sleep(), the children use some of Trio’s networking APIs.

Let’s look at the parent first:

27
28
29
30
31
32
33
34
35
36
async def parent():
    print(f"parent: connecting to 127.0.0.1:{PORT}")
    client_stream = await trio.open_tcp_stream("127.0.0.1", PORT)
    async with client_stream:
        async with trio.open_nursery() as nursery:
            print("parent: spawning sender...")
            nursery.start_soon(sender, client_stream)

            print("parent: spawning receiver...")
            nursery.start_soon(receiver, client_stream)

First we call trio.open_tcp_stream() to make a TCP connection to the server. 127.0.0.1 is a magic IP address meaning “the computer I’m running on”, so this connects us to whatever program on the local computer is using PORT as its contact point. This function returns an object implementing Trio’s Stream interface, which gives us methods to send and receive bytes, and to close the connection when we’re done. We use an async with block to make sure that we do close the connection – not a big deal in a toy example like this, but it’s a good habit to get into, and Trio is designed to make with and async with blocks easy to use.

Finally, we start up two child tasks, and pass each of them a reference to the stream. (This is also a good example of how nursery.start_soon lets you pass positional arguments to the spawned function.)

Our first task’s job is to send data to the server:

12
13
14
15
16
17
18
async def sender(client_stream):
    print("sender: started!")
    while True:
        data = b"async can sometimes be confusing, but I believe in you!"
        print(f"sender: sending {data!r}")
        await client_stream.send_all(data)
        await trio.sleep(1)

It uses a loop that alternates between calling await client_stream.send_all(...) to send some data (this is the method you use for sending data on any kind of Trio stream), and then sleeping for a second to avoid making the output scroll by too fast on your terminal.

And the second task’s job is to process the data the server sends back:

20
21
22
23
24
25
async def receiver(client_stream):
    print("receiver: started!")
    async for data in client_stream:
        print(f"receiver: got data {data!r}")
    print("receiver: connection closed")
    sys.exit()

It uses an async for loop to fetch data from the server. Alternatively, it could use receive_some, which is the opposite of send_all, but using async for saves some boilerplate.

And now we’re ready to look at the server.

An echo server

As usual, let’s look at the whole thing first, and then we’ll discuss the pieces:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# echo-server.py

import trio
from itertools import count

# Port is arbitrary, but:
# - must be in between 1024 and 65535
# - can't be in use by some other program on your computer
# - must match what we set in our echo client
PORT = 12345

CONNECTION_COUNTER = count()

async def echo_server(server_stream):
    # Assign each connection a unique number to make our debug prints easier
    # to understand when there are multiple simultaneous connections.
    ident = next(CONNECTION_COUNTER)
    print(f"echo_server {ident}: started")
    try:
        async for data in server_stream:
            print(f"echo_server {ident}: received data {data!r}")
            await server_stream.send_all(data)
        print(f"echo_server {ident}: connection closed")
    # FIXME: add discussion of MultiErrors to the tutorial, and use
    # MultiError.catch here. (Not important in this case, but important if the
    # server code uses nurseries internally.)
    except Exception as exc:
        # Unhandled exceptions will propagate into our parent and take
        # down the whole program. If the exception is KeyboardInterrupt,
        # that's what we want, but otherwise maybe not...
        print(f"echo_server {ident}: crashed: {exc!r}")

async def main():
    await trio.serve_tcp(echo_server, PORT)

# We could also just write 'trio.run(trio.serve_tcp, echo_server, PORT)', but real
# programs almost always end up doing other stuff too and then we'd have to go
# back and factor it out into a separate function anyway. So it's simplest to
# just make it a standalone function from the beginning.
trio.run(main)

Let’s start with main, which is just one line long:

33
34
async def main():
    await trio.serve_tcp(echo_server, PORT)

What this does is call serve_tcp(), which is a convenience function Trio provides that runs forever (or at least until you hit control-C or otherwise cancel it). This function does several helpful things:

  • It creates a nursery internally, so that our server will be able to handle multiple connections at the same time.

  • It listens for incoming TCP connections on the specified PORT.

  • Whenever a connection arrives, it starts a new task running the function we pass (in this example it’s echo_server), and passes it a stream representing that connection.

  • When each task exits, it makes sure to close the corresponding connection. (That’s why you don’t see any async with server_stream in the server – serve_tcp() takes care of this for us.)

So serve_tcp() is pretty handy! This part works pretty much the same for any server, whether it’s an echo server, HTTP server, SSH server, or whatever, so it makes sense to bundle it all up together in a helper function like this.

Now let’s look at echo_server, which handles each client connection – so if there are multiple clients, there might be multiple calls to echo_server running at the same time. This is where we implement our server’s “echo” behavior. This should be pretty straightforward to understand, because it uses the same stream functions we saw in the last section:

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
async def echo_server(server_stream):
    # Assign each connection a unique number to make our debug prints easier
    # to understand when there are multiple simultaneous connections.
    ident = next(CONNECTION_COUNTER)
    print(f"echo_server {ident}: started")
    try:
        async for data in server_stream:
            print(f"echo_server {ident}: received data {data!r}")
            await server_stream.send_all(data)
        print(f"echo_server {ident}: connection closed")
    # FIXME: add discussion of MultiErrors to the tutorial, and use
    # MultiError.catch here. (Not important in this case, but important if the
    # server code uses nurseries internally.)
    except Exception as exc:
        # Unhandled exceptions will propagate into our parent and take
        # down the whole program. If the exception is KeyboardInterrupt,
        # that's what we want, but otherwise maybe not...
        print(f"echo_server {ident}: crashed: {exc!r}")

The argument server_stream is provided by serve_tcp(), and is the other end of the connection we made in the client: so the data that the client passes to send_all will come out here. Then we have a try block discussed below, and finally the server loop which alternates between reading some data from the socket and then sending it back out again (unless the socket was closed, in which case we quit).

So what’s that try block for? Remember that in Trio, like Python in general, exceptions keep propagating until they’re caught. Here we think it’s plausible there might be unexpected exceptions, and we want to isolate that to making just this one task crash, without taking down the whole program. For example, if the client closes the connection at the wrong moment then it’s possible this code will end up calling send_all on a closed connection and get a BrokenResourceError; that’s unfortunate, and in a more serious program we might want to handle it more explicitly, but it doesn’t indicate a problem for any other connections. On the other hand, if the exception is something like a KeyboardInterrupt, we do want that to propagate out into the parent task and cause the whole program to exit. To express this, we use a try block with an except Exception: handler.

In general, Trio leaves it up to you to decide whether and how you want to handle exceptions, just like Python in general.

Try it out

Open a few terminals, run echo-server.py in one, run echo-client.py in another, and watch the messages scroll by! When you get bored, you can exit by hitting control-C.

Some things to try:

  • Open several terminals, and run multiple clients at the same time, all talking to the same server.

  • See how the server reacts when you hit control-C on the client.

  • See how the client reacts when you hit control-C on the server.

Flow control in our echo client and server

Here’s a question you might be wondering about: why does our client use two separate tasks for sending and receiving, instead of a single task that alternates between them – like the server has? For example, our client could use a single task like:

# Can you spot the two problems with this code?
async def send_and_receive(client_stream):
    while True:
        data = ...
        await client_stream.send_all(data)
        received = await client_stream.receive_some()
        if not received:
            sys.exit()
        await trio.sleep(1)

It turns out there are two problems with this – one minor and one major. Both relate to flow control. The minor problem is that when we call receive_some here we’re not waiting for all the data to be available; receive_some returns as soon as any data is available. If data is small, then our operating systems / network / server will probably keep it all together in a single chunk, but there’s no guarantee. If the server sends hello then we might get hello, or hel lo, or h e l l o, or … bottom line, any time we’re expecting more than one byte of data, we have to be prepared to call receive_some multiple times.

And where this would go especially wrong is if we find ourselves in the situation where data is big enough that it passes some internal threshold, and the operating system or network decide to always break it up into multiple pieces. Now on each pass through the loop, we send len(data) bytes, but read less than that. The result is something like a memory leak: we’ll end up with more and more data backed up in the network, until eventually something breaks.

Note

If you’re curious how things break, then you can use receive_some's optional argument to put a limit on how many bytes you read each time, and see what happens.

We could fix this by keeping track of how much data we’re expecting at each moment, and then keep calling receive_some until we get it all:

expected = len(data)
while expected > 0:
    received = await client_stream.receive_some(expected)
    if not received:
        sys.exit(1)
    expected -= len(received)

This is a bit cumbersome, but it would solve this problem.

There’s another problem, though, that’s deeper. We’re still alternating between sending and receiving. Notice that when we send data, we use await: this means that sending can potentially block. Why does this happen? Any data that we send goes first into an operating system buffer, and from there onto the network, and then another operating system buffer on the receiving computer, before the receiving program finally calls receive_some to take the data out of these buffers. If we call send_all with a small amount of data, then it goes into these buffers and send_all returns immediately. But if we send enough data fast enough, eventually the buffers fill up, and send_all will block until the remote side calls receive_some and frees up some space.

Now let’s think about this from the server’s point of view. Each time it calls receive_some, it gets some data that it needs to send back. And until it sends it back, the data that is sitting around takes up memory. Computers have finite amounts of RAM, so if our server is well behaved then at some point it needs to stop calling receive_some until it gets rid of some of the old data by doing its own call to send_all. So for the server, really the only viable option is to alternate between receiving and sending.

But we need to remember that it’s not just the client’s call to send_all that might block: the server’s call to send_all can also get into a situation where it blocks until the client calls receive_some. So if the server is waiting for send_all to finish before it calls receive_some, and our client also waits for send_all to finish before it calls receive_some,… we have a problem! The client won’t call receive_some until the server has called receive_some, and the server won’t call receive_some until the client has called receive_some. If our client is written to alternate between sending and receiving, and the chunk of data it’s trying to send is large enough (e.g. 10 megabytes will probably do it in most configurations), then the two processes will deadlock.

Moral: Trio gives you powerful tools to manage sequential and concurrent execution. In this example we saw that the server needs send and receive_some to alternate in sequence, while the client needs them to run concurrently, and both were straightforward to implement. But when you’re implementing network code like this then it’s important to think carefully about flow control and buffering, because it’s up to you to choose the right execution mode!

Other popular async libraries like Twisted and asyncio tend to paper over these kinds of issues by throwing in unbounded buffers everywhere. This can avoid deadlocks, but can introduce its own problems and in particular can make it difficult to keep memory usage and latency under control. While both approaches have their advantages, Trio takes the position that it’s better to expose the underlying problem as directly as possible and provide good tools to confront it head-on.

Note

If you want to try and make the deadlock happen on purpose to see for yourself, and you’re using Windows, then you might need to split the send_all call up into two calls that each send half of the data. This is because Windows has a somewhat unusual way of handling buffering.

When things go wrong: timeouts, cancellation and exceptions in concurrent tasks

TODO: give an example using fail_after()

TODO: explain Cancelled

TODO: explain how cancellation is also used when one child raises an exception

TODO: show an example MultiError traceback and walk through its structure

TODO: maybe a brief discussion of KeyboardInterrupt handling?

Awesome Trio Libraries

You have completed the tutorial, and are enthusiastic about building great new applications and libraries with async functionality. However, to get much useful work done you will want to use some of the great libraries that support Trio-flavoured concurrency. This list is not complete, but gives a starting point. Another great way to find Trio-compatible libraries is to search on PyPI for the Framework :: Trio tag -> PyPI Search

Getting Started

  • cookiecutter-trio - This is a cookiecutter template for Python projects that use Trio. It makes it easy to start a new project, by providing a bunch of preconfigured boilerplate.

  • pytest-trio - Pytest plugin to test async-enabled Trio functions.

  • sphinxcontrib-trio - Make Sphinx better at documenting Python functions and methods. In particular, it makes it easy to document async functions.

Web and HTML

  • httpx - HTTPX is a fully featured HTTP client for Python 3, which provides sync and async APIs, and support for both HTTP/1.1 and HTTP/2.

  • trio-websocket - A WebSocket client and server implementation striving for safety, correctness, and ergonomics.

  • quart-trio - Like Flask, but for Trio. A simple and powerful framework for building async web applications and REST APIs. Tip: this is an ASGI-based framework, so you’ll also need an HTTP server with ASGI support.

  • hypercorn - An HTTP server for hosting your ASGI apps. Supports HTTP/1.1, HTTP/2, HTTP/3, and Websockets. Can be run as a standalone server, or embedded in a larger Trio app. Use it with quart-trio, or any other Trio-compatible ASGI framework.

  • DeFramed - DeFramed is a Web non-framework that supports a 99%-server-centric approach to Web coding, including support for the Remi GUI library.

  • pura - A simple web framework for embedding realtime graphical visualization into Trio apps, enabling inspection and manipulation of program state during development.

  • pyscalpel - A fast and powerful webscraping library.

  • muffin - Muffin is a fast, simple ASGI web-framework

  • asgi-tools - Tools to quickly build lightest ASGI apps (also contains a test client with lifespan, websocket support)

  • starlette - The little ASGI framework that shines.

Database

  • triopg - PostgreSQL client for Trio based on asyncpg.

  • trio-mysql - Pure Python MySQL Client.

  • sqlalchemy_aio - Add asyncio and Trio support to SQLAlchemy core, derived from alchimia.

  • redio - Redis client, pure Python and Trio.

  • trio_redis - A Redis client for Trio. Depends on hiredis-py.

  • asyncakumuli - Client for the Akumuli time series database.

  • aio-databases - Async Support for various databases (triopg, trio-mysql)

  • peewee-aio - Peewee Async ORM with trio support (triopg, trio-mysql).

IOT

  • DistMQTT - DistMQTT is an open source MQTT client and broker implementation. It is a fork of hbmqtt with support for anyio and DistKV.

  • asyncgpio - Allows easy access to the GPIO pins on your Raspberry Pi or similar embedded computer.

  • asyncowfs - High-level, object-oriented access to 1wire sensors and actors.

  • DistKV - a persistent, distributed, master-less key/value storage with async notification and some IoT-related plug-ins.

Building Command Line Apps

  • trio-click - Python composable command line utility, trio-compatible version.

  • urwid - Urwid is a console user interface library for Python.

Building GUI Apps

  • QTrio - Integration between Trio and either the PyQt or PySide Qt wrapper libraries. Uses Trio’s guest mode.

Multi-Core/Multiprocessing

  • tractor - An experimental, trionic (aka structured concurrent) “actor model” for distributed multi-core Python.

  • Trio run_in_process - Trio based API for running code in a separate process.

  • trio-parallel - CPU parallelism for Trio

Stream Processing

  • Slurry - Slurry is a microframework for building reactive, data processing applications with Trio.

RPC

  • purepc - Native, async Python gRPC client and server implementation using anyio.

Testing

  • pytest-trio - Pytest plugin for trio.

  • hypothesis-trio - Hypothesis plugin for trio.

  • trustme - #1 quality TLS certs while you wait, for the discerning tester.

  • pytest-aio - Pytest plugin with support for trio, curio, asyncio

Tools and Utilities

  • trio-typing - Type hints for Trio and related projects.

  • trio-util - An assortment of utilities for the Trio async/await framework.

  • tricycle - This is a library of interesting-but-maybe-not-yet-fully-proven extensions to Trio.

  • tenacity - Retrying library for Python with async/await support.

  • perf-timer - A code timer with Trio async support (see TrioPerfTimer). Collects execution time of a block of code excluding time when the coroutine isn’t scheduled, such as during blocking I/O and sleep. Also offers trio_perf_counter() for low-level timing.

  • aiometer - Execute lots of tasks concurrently while controlling concurrency limits

  • triotp - OTP framework for Python Trio

Trio/Asyncio Interoperability

  • anyio - AnyIO is a asynchronous compatibility API that allows applications and libraries written against it to run unmodified on asyncio, curio and trio.

  • sniffio - This is a tiny package whose only purpose is to let you detect which async library your code is running under.

  • trio-asyncio - Trio-Asyncio lets you use many asyncio libraries from your Trio app.

Trio’s core functionality

Entering Trio

If you want to use Trio, then the first thing you have to do is call trio.run():

trio.run(async_fn, *args, clock=None, instruments=(), restrict_keyboard_interrupt_to_checkpoints=False)

Run a Trio-flavored async function, and return the result.

Calling:

run(async_fn, *args)

is the equivalent of:

await async_fn(*args)

except that run() can (and must) be called from a synchronous context.

This is Trio’s main entry point. Almost every other function in Trio requires that you be inside a call to run().

Parameters
  • async_fn – An async function.

  • args – Positional arguments to be passed to async_fn. If you need to pass keyword arguments, then use functools.partial().

  • clockNone to use the default system-specific monotonic clock; otherwise, an object implementing the trio.abc.Clock interface, like (for example) a trio.testing.MockClock instance.

  • instruments (list of trio.abc.Instrument objects) – Any instrumentation you want to apply to this run. This can also be modified during the run; see Instrument API.

  • restrict_keyboard_interrupt_to_checkpoints (bool) –

    What happens if the user hits control-C while run() is running? If this argument is False (the default), then you get the standard Python behavior: a KeyboardInterrupt exception will immediately interrupt whatever task is running (or if no task is running, then Trio will wake up a task to be interrupted). Alternatively, if you set this argument to True, then KeyboardInterrupt delivery will be delayed: it will be only be raised at checkpoints, like a Cancelled exception.

    The default behavior is nice because it means that even if you accidentally write an infinite loop that never executes any checkpoints, then you can still break out of it using control-C. The alternative behavior is nice if you’re paranoid about a KeyboardInterrupt at just the wrong place leaving your program in an inconsistent state, because it means that you only have to worry about KeyboardInterrupt at the exact same places where you already have to worry about Cancelled.

    This setting has no effect if your program has registered a custom SIGINT handler, or if run() is called from anywhere but the main thread (this is a Python limitation), or if you use open_signal_receiver() to catch SIGINT.

Returns

Whatever async_fn returns.

Raises
  • TrioInternalError – if an unexpected error is encountered inside Trio’s internal machinery. This is a bug and you should let us know.

  • Anything else – if async_fn raises an exception, then run() propagates it.

General principles

Checkpoints

When writing code using Trio, it’s very important to understand the concept of a checkpoint. Many of Trio’s functions act as checkpoints.

A checkpoint is two things:

  1. It’s a point where Trio checks for cancellation. For example, if the code that called your function set a timeout, and that timeout has expired, then the next time your function executes a checkpoint Trio will raise a Cancelled exception. See Cancellation and timeouts below for more details.

  2. It’s a point where the Trio scheduler checks its scheduling policy to see if it’s a good time to switch to another task, and potentially does so. (Currently, this check is very simple: the scheduler always switches at every checkpoint. But this might change in the future.)

When writing Trio code, you need to keep track of where your checkpoints are. Why? First, because checkpoints require extra scrutiny: whenever you execute a checkpoint, you need to be prepared to handle a Cancelled error, or for another task to run and rearrange some state out from under you. And second, because you also need to make sure that you have enough checkpoints: if your code doesn’t pass through a checkpoint on a regular basis, then it will be slow to notice and respond to cancellation and – much worse – since Trio is a cooperative multi-tasking system where the only place the scheduler can switch tasks is at checkpoints, it’ll also prevent the scheduler from fairly allocating time between different tasks and adversely effect the response latency of all the other code running in the same process. (Informally we say that a task that does this is “hogging the run loop”.)

So when you’re doing code review on a project that uses Trio, one of the things you’ll want to think about is whether there are enough checkpoints, and whether each one is handled correctly. Of course this means you need a way to recognize checkpoints. How do you do that? The underlying principle is that any operation that blocks has to be a checkpoint. This makes sense: if an operation blocks, then it might block for a long time, and you’ll want to be able to cancel it if a timeout expires; and in any case, while this task is blocked we want another task to be scheduled to run so our code can make full use of the CPU.

But if we want to write correct code in practice, then this principle is a little too sloppy and imprecise to be useful. How do we know which functions might block? What if a function blocks sometimes, but not others, depending on the arguments passed / network speed / phase of the moon? How do we figure out where the checkpoints are when we’re stressed and sleep deprived but still want to get this code review right, and would prefer to reserve our mental energy for thinking about the actual logic instead of worrying about checkpoints?

Don’t worry – Trio’s got your back. Since checkpoints are important and ubiquitous, we make it as simple as possible to keep track of them. Here are the rules:

  • Regular (synchronous) functions never contain any checkpoints.

  • If you call an async function provided by Trio (await <something in trio>), and it doesn’t raise an exception, then it always acts as a checkpoint. (If it does raise an exception, it might act as a checkpoint or might not.)

    • This includes async iterators: If you write async for ... in <a trio object>, then there will be at least one checkpoint before each iteration of the loop and one checkpoint after the last iteration.

    • Partial exception for async context managers: Both the entry and exit of an async with block are defined as async functions; but for a particular type of async context manager, it’s often the case that only one of them is able to block, which means only that one will act as a checkpoint. This is documented on a case-by-case basis.

  • Third-party async functions / iterators / context managers can act as checkpoints; if you see await <something> or one of its friends, then that might be a checkpoint. So to be safe, you should prepare for scheduling or cancellation happening there.

The reason we distinguish between Trio functions and other functions is that we can’t make any guarantees about third party code. Checkpoint-ness is a transitive property: if function A acts as a checkpoint, and you write a function that calls function A, then your function also acts as a checkpoint. If you don’t, then it isn’t. So there’s nothing stopping someone from writing a function like:

# technically legal, but bad style:
async def why_is_this_async():
    return 7

that never calls any of Trio’s async functions. This is an async function, but it’s not a checkpoint. But why make a function async if it never calls any async functions? It’s possible, but it’s a bad idea. If you have a function that’s not calling any async functions, then you should make it synchronous. The people who use your function will thank you, because it makes it obvious that your function is not a checkpoint, and their code reviews will go faster.

(Remember how in the tutorial we emphasized the importance of the “async sandwich”, and the way it means that await ends up being a marker that shows when you’re calling a function that calls a function that … eventually calls one of Trio’s built-in async functions? The transitivity of async-ness is a technical requirement that Python imposes, but since it exactly matches the transitivity of checkpoint-ness, we’re able to exploit it to help you keep track of checkpoints. Pretty sneaky, eh?)

A slightly trickier case is a function like:

async def sleep_or_not(should_sleep):
    if should_sleep:
        await trio.sleep(1)
    else:
        pass

Here the function acts as a checkpoint if you call it with should_sleep set to a true value, but not otherwise. This is why we emphasize that Trio’s own async functions are unconditional checkpoints: they always check for cancellation and check for scheduling, regardless of what arguments they’re passed. If you find an async function in Trio that doesn’t follow this rule, then it’s a bug and you should let us know.

Inside Trio, we’re very picky about this, because Trio is the foundation of the whole system so we think it’s worth the extra effort to make things extra predictable. It’s up to you how picky you want to be in your code. To give you a more realistic example of what this kind of issue looks like in real life, consider this function:

async def recv_exactly(sock, nbytes):
    data = bytearray()
    while nbytes > 0:
        # recv() reads up to 'nbytes' bytes each time
        chunk = await sock.recv(nbytes)
        if not chunk:
            raise RuntimeError("socket unexpected closed")
        nbytes -= len(chunk)
        data += chunk
    return data

If called with an nbytes that’s greater than zero, then it will call sock.recv at least once, and recv is an async Trio function, and thus an unconditional checkpoint. So in this case, recv_exactly acts as a checkpoint. But if we do await recv_exactly(sock, 0), then it will immediately return an empty buffer without executing a checkpoint. If this were a function in Trio itself, then this wouldn’t be acceptable, but you may decide you don’t want to worry about this kind of minor edge case in your own code.

If you do want to be careful, or if you have some CPU-bound code that doesn’t have enough checkpoints in it, then it’s useful to know that await trio.sleep(0) is an idiomatic way to execute a checkpoint without doing anything else, and that trio.testing.assert_checkpoints() can be used to test that an arbitrary block of code contains a checkpoint.

Thread safety

The vast majority of Trio’s API is not thread safe: it can only be used from inside a call to trio.run(). This manual doesn’t bother documenting this on individual calls; unless specifically noted otherwise, you should assume that it isn’t safe to call any Trio functions from anywhere except the Trio thread. (But see below if you really do need to work with threads.)

Time and clocks

Every call to run() has an associated clock.

By default, Trio uses an unspecified monotonic clock, but this can be changed by passing a custom clock object to run() (e.g. for testing).

You should not assume that Trio’s internal clock matches any other clock you have access to, including the clocks of simultaneous calls to trio.run() happening in other processes or threads!

The default clock is currently implemented as time.perf_counter() plus a large random offset. The idea here is to catch code that accidentally uses time.perf_counter() early, which should help keep our options open for changing the clock implementation later, and (more importantly) make sure you can be confident that custom clocks like trio.testing.MockClock will work with third-party libraries you don’t control.

trio.current_time()

Returns the current time according to Trio’s internal clock.

Returns

The current time.

Return type

float

Raises

RuntimeError – if not inside a call to trio.run().

await trio.sleep(seconds)

Pause execution of the current task for the given number of seconds.

Parameters

seconds (float) – The number of seconds to sleep. May be zero to insert a checkpoint without actually blocking.

Raises

ValueError – if seconds is negative.

await trio.sleep_until(deadline)

Pause execution of the current task until the given time.

The difference between sleep() and sleep_until() is that the former takes a relative time and the latter takes an absolute time according to Trio’s internal clock (as returned by current_time()).

Parameters

deadline (float) – The time at which we should wake up again. May be in the past, in which case this function executes a checkpoint but does not block.

await trio.sleep_forever()

Pause execution of the current task forever (or until cancelled).

Equivalent to calling await sleep(math.inf).

If you’re a mad scientist or otherwise feel the need to take direct control over the PASSAGE OF TIME ITSELF, then you can implement a custom Clock class:

class trio.abc.Clock

The interface for custom run loop clocks.

abstractmethod current_time()

Return the current time, according to this clock.

This is used to implement functions like trio.current_time() and trio.move_on_after().

Returns

The current time.

Return type

float

abstractmethod deadline_to_sleep_time(deadline)

Compute the real time until the given deadline.

This is called before we enter a system-specific wait function like select.select(), to get the timeout to pass.

For a clock using wall-time, this should be something like:

return deadline - self.current_time()

but of course it may be different if you’re implementing some kind of virtual clock.

Parameters

deadline (float) – The absolute time of the next deadline, according to this clock.

Returns

The number of real seconds to sleep until the given deadline. May be math.inf.

Return type

float

abstractmethod start_clock()

Do any setup this clock might need.

Called at the beginning of the run.

Cancellation and timeouts

Trio has a rich, composable system for cancelling work, either explicitly or when a timeout expires.

A simple timeout example

In the simplest case, you can apply a timeout to a block of code:

with trio.move_on_after(30):
    result = await do_http_get("https://...")
    print("result is", result)
print("with block finished")

We refer to move_on_after() as creating a “cancel scope”, which contains all the code that runs inside the with block. If the HTTP request takes more than 30 seconds to run, then it will be cancelled: we’ll abort the request and we won’t see result is ... printed on the console; instead we’ll go straight to printing the with block finished message.

Note

Note that this is a single 30 second timeout for the entire body of the with statement. This is different from what you might have seen with other Python libraries, where timeouts often refer to something more complicated. We think this way is easier to reason about.

How does this work? There’s no magic here: Trio is built using ordinary Python functionality, so we can’t just abandon the code inside the with block. Instead, we take advantage of Python’s standard way of aborting a large and complex piece of code: we raise an exception.

Here’s the idea: whenever you call a cancellable function like await trio.sleep(...) or await sock.recv(...) – see Checkpoints – then the first thing that function does is to check if there’s a surrounding cancel scope whose timeout has expired, or otherwise been cancelled. If so, then instead of performing the requested operation, the function fails immediately with a Cancelled exception. In this example, this probably happens somewhere deep inside the bowels of do_http_get. The exception then propagates out like any normal exception (you could even catch it if you wanted, but that’s generally a bad idea), until it reaches the with move_on_after(...):. And at this point, the Cancelled exception has done its job – it’s successfully unwound the whole cancelled scope – so move_on_after() catches it, and execution continues as normal after the with block. And this all works correctly even if you have nested cancel scopes, because every Cancelled object carries an invisible marker that makes sure that the cancel scope that triggered it is the only one that will catch it.

Handling cancellation

Pretty much any code you write using Trio needs to have some strategy to handle Cancelled exceptions – even if you didn’t set a timeout, then your caller might (and probably will).

You can catch Cancelled, but you shouldn’t! Or more precisely, if you do catch it, then you should do some cleanup and then re-raise it or otherwise let it continue propagating (unless you encounter an error, in which case it’s OK to let that propagate instead). To help remind you of this fact, Cancelled inherits from BaseException, like KeyboardInterrupt and SystemExit do, so that it won’t be caught by catch-all except Exception: blocks.

It’s also important in any long-running code to make sure that you regularly check for cancellation, because otherwise timeouts won’t work! This happens implicitly every time you call a cancellable operation; see below for details. If you have a task that has to do a lot of work without any I/O, then you can use await sleep(0) to insert an explicit cancel+schedule point.

Here’s a rule of thumb for designing good Trio-style (“trionic”?) APIs: if you’re writing a reusable function, then you shouldn’t take a timeout= parameter, and instead let your caller worry about it. This has several advantages. First, it leaves the caller’s options open for deciding how they prefer to handle timeouts – for example, they might find it easier to work with absolute deadlines instead of relative timeouts. If they’re the ones calling into the cancellation machinery, then they get to pick, and you don’t have to worry about it. Second, and more importantly, this makes it easier for others to re-use your code. If you write a http_get function, and then I come along later and write a log_in_to_twitter function that needs to internally make several http_get calls, I don’t want to have to figure out how to configure the individual timeouts on each of those calls – and with Trio’s timeout system, it’s totally unnecessary.

Of course, this rule doesn’t apply to APIs that need to impose internal timeouts. For example, if you write a start_http_server function, then you probably should give your caller some way to configure timeouts on individual requests.

Cancellation semantics

You can freely nest cancellation blocks, and each Cancelled exception “knows” which block it belongs to. So long as you don’t stop it, the exception will keep propagating until it reaches the block that raised it, at which point it will stop automatically.

Here’s an example:

print("starting...")
with trio.move_on_after(5):
    with trio.move_on_after(10):
        await trio.sleep(20)
        print("sleep finished without error")
    print("move_on_after(10) finished without error")
print("move_on_after(5) finished without error")

In this code, the outer scope will expire after 5 seconds, causing the sleep() call to return early with a Cancelled exception. Then this exception will propagate through the with move_on_after(10) line until it’s caught by the with move_on_after(5) context manager. So this code will print:

starting...
move_on_after(5) finished without error

The end result is that Trio has successfully cancelled exactly the work that was happening within the scope that was cancelled.

Looking at this, you might wonder how you can tell whether the inner block timed out – perhaps you want to do something different, like try a fallback procedure or report a failure to our caller. To make this easier, move_on_after()´s __enter__ function returns an object representing this cancel scope, which we can use to check whether this scope caught a Cancelled exception:

with trio.move_on_after(5) as cancel_scope:
    await trio.sleep(10)
print(cancel_scope.cancelled_caught)  # prints "True"

The cancel_scope object also allows you to check or adjust this scope’s deadline, explicitly trigger a cancellation without waiting for the deadline, check if the scope has already been cancelled, and so forth – see CancelScope below for the full details.

Cancellations in Trio are “level triggered”, meaning that once a block has been cancelled, all cancellable operations in that block will keep raising Cancelled. This helps avoid some pitfalls around resource clean-up. For example, imagine that we have a function that connects to a remote server and sends some messages, and then cleans up on the way out:

with trio.move_on_after(TIMEOUT):
    conn = make_connection()
    try:
        await conn.send_hello_msg()
    finally:
        await conn.send_goodbye_msg()

Now suppose that the remote server stops responding, so our call to await conn.send_hello_msg() hangs forever. Fortunately, we were clever enough to put a timeout around this code, so eventually the timeout will expire and send_hello_msg will raise Cancelled. But then, in the finally block, we make another blocking operation, which will also hang forever! At this point, if we were using asyncio or another library with “edge-triggered” cancellation, we’d be in trouble: since our timeout already fired, it wouldn’t fire again, and at this point our application would lock up forever. But in Trio, this doesn’t happen: the await conn.send_goodbye_msg() call is still inside the cancelled block, so it will also raise Cancelled.

Of course, if you really want to make another blocking call in your cleanup handler, Trio will let you; it’s trying to prevent you from accidentally shooting yourself in the foot. Intentional foot-shooting is no problem (or at least – it’s not Trio’s problem). To do this, create a new scope, and set its shield attribute to True:

with trio.move_on_after(TIMEOUT):
    conn = make_connection()
    try:
        await conn.send_hello_msg()
    finally:
        with trio.move_on_after(CLEANUP_TIMEOUT) as cleanup_scope:
            cleanup_scope.shield = True
            await conn.send_goodbye_msg()

So long as you’re inside a scope with shield = True set, then you’ll be protected from outside cancellations. Note though that this only applies to outside cancellations: if CLEANUP_TIMEOUT expires then await conn.send_goodbye_msg() will still be cancelled, and if await conn.send_goodbye_msg() call uses any timeouts internally, then those will continue to work normally as well. This is a pretty advanced feature that most people probably won’t use, but it’s there for the rare cases where you need it.

Cancellation and primitive operations

We’ve talked a lot about what happens when an operation is cancelled, and how you need to be prepared for this whenever calling a cancellable operation… but we haven’t gone into the details about which operations are cancellable, and how exactly they behave when they’re cancelled.

Here’s the rule: if it’s in the trio namespace, and you use await to call it, then it’s cancellable (see Checkpoints above). Cancellable means:

  • If you try to call it when inside a cancelled scope, then it will raise Cancelled.

  • If it blocks, and while it’s blocked then one of the scopes around it becomes cancelled, it will return early and raise Cancelled.

  • Raising Cancelled means that the operation did not happen. If a Trio socket’s send method raises Cancelled, then no data was sent. If a Trio socket’s recv method raises Cancelled then no data was lost – it’s still sitting in the socket receive buffer waiting for you to call recv again. And so forth.

There are a few idiosyncratic cases where external constraints make it impossible to fully implement these semantics. These are always documented. There is also one systematic exception:

  • Async cleanup operations – like __aexit__ methods or async close methods – are cancellable just like anything else except that if they are cancelled, they still perform a minimum level of cleanup before raising Cancelled.

For example, closing a TLS-wrapped socket normally involves sending a notification to the remote peer, so that they can be cryptographically assured that you really meant to close the socket, and your connection wasn’t just broken by a man-in-the-middle attacker. But handling this robustly is a bit tricky. Remember our example above where the blocking send_goodbye_msg caused problems? That’s exactly how closing a TLS socket works: if the remote peer has disappeared, then our code may never be able to actually send our shutdown notification, and it would be nice if it didn’t block forever trying. Therefore, the method for closing a TLS-wrapped socket will try to send that notification – and if it gets cancelled, then it will give up on sending the message, but will still close the underlying socket before raising Cancelled, so at least you don’t leak that resource.

Cancellation API details

move_on_after() and all the other cancellation facilities provided by Trio are ultimately implemented in terms of CancelScope objects.

class trio.CancelScope(*, deadline=inf, shield=False)

A cancellation scope: the link between a unit of cancellable work and Trio’s cancellation system.

A CancelScope becomes associated with some cancellable work when it is used as a context manager surrounding that work:

cancel_scope = trio.CancelScope()
...
with cancel_scope:
    await long_running_operation()

Inside the with block, a cancellation of cancel_scope (via a call to its cancel() method or via the expiry of its deadline) will immediately interrupt the long_running_operation() by raising Cancelled at its next checkpoint.

The context manager __enter__ returns the CancelScope object itself, so you can also write with trio.CancelScope() as cancel_scope:.

If a cancel scope becomes cancelled before entering its with block, the Cancelled exception will be raised at the first checkpoint inside the with block. This allows a CancelScope to be created in one task and passed to another, so that the first task can later cancel some work inside the second.

Cancel scopes are not reusable or reentrant; that is, each cancel scope can be used for at most one with block. (You’ll get a RuntimeError if you violate this rule.)

The CancelScope constructor takes initial values for the cancel scope’s deadline and shield attributes; these may be freely modified after construction, whether or not the scope has been entered yet, and changes take immediate effect.

deadline

Read-write, float. An absolute time on the current run’s clock at which this scope will automatically become cancelled. You can adjust the deadline by modifying this attribute, e.g.:

# I need a little more time!
cancel_scope.deadline += 30

Note that for efficiency, the core run loop only checks for expired deadlines every once in a while. This means that in certain cases there may be a short delay between when the clock says the deadline should have expired, and when checkpoints start raising Cancelled. This is a very obscure corner case that you’re unlikely to notice, but we document it for completeness. (If this does cause problems for you, of course, then we want to know!)

Defaults to math.inf, which means “no deadline”, though this can be overridden by the deadline= argument to the CancelScope constructor.

shield

Read-write, bool, default False. So long as this is set to True, then the code inside this scope will not receive Cancelled exceptions from scopes that are outside this scope. They can still receive Cancelled exceptions from (1) this scope, or (2) scopes inside this scope. You can modify this attribute:

with trio.CancelScope() as cancel_scope:
    cancel_scope.shield = True
    # This cannot be interrupted by any means short of
    # killing the process:
    await sleep(10)

    cancel_scope.shield = False
    # Now this can be cancelled normally:
    await sleep(10)

Defaults to False, though this can be overridden by the shield= argument to the CancelScope constructor.

cancel()

Cancels this scope immediately.

This method is idempotent, i.e., if the scope was already cancelled then this method silently does nothing.

cancelled_caught

Readonly bool. Records whether this scope caught a Cancelled exception. This requires two things: (1) the with block exited with a Cancelled exception, and (2) this scope is the one that was responsible for triggering this Cancelled exception.

cancel_called

Readonly bool. Records whether cancellation has been requested for this scope, either by an explicit call to cancel() or by the deadline expiring.

This attribute being True does not necessarily mean that the code within the scope has been, or will be, affected by the cancellation. For example, if cancel() was called after the last checkpoint in the with block, when it’s too late to deliver a Cancelled exception, then this attribute will still be True.

This attribute is mostly useful for debugging and introspection. If you want to know whether or not a chunk of code was actually cancelled, then cancelled_caught is usually more appropriate.

Often there is no need to create CancelScope object. Trio already includes cancel_scope attribute in a task-related Nursery object. We will cover nurseries later in the manual.

Trio also provides several convenience functions for the common situation of just wanting to impose a timeout on some code:

with trio.move_on_after(seconds) as cancel_scope

Use as a context manager to create a cancel scope whose deadline is set to now + seconds.

Parameters

seconds (float) – The timeout.

Raises

ValueError – if timeout is less than zero.

with trio.move_on_at(deadline) as cancel_scope

Use as a context manager to create a cancel scope with the given absolute deadline.

Parameters

deadline (float) – The deadline.

with trio.fail_after(seconds) as cancel_scope

Creates a cancel scope with the given timeout, and raises an error if it is actually cancelled.

This function and move_on_after() are similar in that both create a cancel scope with a given timeout, and if the timeout expires then both will cause Cancelled to be raised within the scope. The difference is that when the Cancelled exception reaches move_on_after(), it’s caught and discarded. When it reaches fail_after(), then it’s caught and TooSlowError is raised in its place.

Raises
with trio.fail_at(deadline) as cancel_scope

Creates a cancel scope with the given deadline, and raises an error if it is actually cancelled.

This function and move_on_at() are similar in that both create a cancel scope with a given absolute deadline, and if the deadline expires then both will cause Cancelled to be raised within the scope. The difference is that when the Cancelled exception reaches move_on_at(), it’s caught and discarded. When it reaches fail_at(), then it’s caught and TooSlowError is raised in its place.

Raises

TooSlowError – if a Cancelled exception is raised in this scope and caught by the context manager.

Cheat sheet:

  • If you want to impose a timeout on a function, but you don’t care whether it timed out or not:

    with trio.move_on_after(TIMEOUT):
        await do_whatever()
    # carry on!
    
  • If you want to impose a timeout on a function, and then do some recovery if it timed out:

    with trio.move_on_after(TIMEOUT) as cancel_scope:
        await do_whatever()
    if cancel_scope.cancelled_caught:
        # The operation timed out, try something else
        try_to_recover()
    
  • If you want to impose a timeout on a function, and then if it times out then just give up and raise an error for your caller to deal with:

    with trio.fail_after(TIMEOUT):
        await do_whatever()
    

It’s also possible to check what the current effective deadline is, which is sometimes useful:

trio.current_effective_deadline()

Returns the current effective deadline for the current task.

This function examines all the cancellation scopes that are currently in effect (taking into account shielding), and returns the deadline that will expire first.

One example of where this might be is useful is if your code is trying to decide whether to begin an expensive operation like an RPC call, but wants to skip it if it knows that it can’t possibly complete in the available time. Another example would be if you’re using a protocol like gRPC that propagates timeout information to the remote peer; this function gives a way to fetch that information so you can send it along.

If this is called in a context where a cancellation is currently active (i.e., a blocking call will immediately raise Cancelled), then returned deadline is -inf. If it is called in a context where no scopes have a deadline set, it returns inf.

Returns

the effective deadline, as an absolute time.

Return type

float

Tasks let you do multiple things at once

One of Trio’s core design principles is: no implicit concurrency. Every function executes in a straightforward, top-to-bottom manner, finishing each operation before moving on to the next – like Guido intended.

But, of course, the entire point of an async library is to let you do multiple things at once. The one and only way to do that in Trio is through the task spawning interface. So if you want your program to walk and chew gum, this is the section for you.

Nurseries and spawning

Most libraries for concurrent programming let you start new child tasks (or threads, or whatever) willy-nilly, whenever and where-ever you feel like it. Trio is a bit different: you can’t start a child task unless you’re prepared to be a responsible parent. The way you demonstrate your responsibility is by creating a nursery:

async with trio.open_nursery() as nursery:
    ...

And once you have a reference to a nursery object, you can start children in that nursery:

async def child():
    ...

async def parent():
    async with trio.open_nursery() as nursery:
        # Make two concurrent calls to child()
        nursery.start_soon(child)
        nursery.start_soon(child)

This means that tasks form a tree: when you call run(), then this creates an initial task, and all your other tasks will be children, grandchildren, etc. of the initial task.

Essentially, the body of the async with block acts like an initial task that’s running inside the nursery, and then each call to nursery.start_soon adds another task that runs in parallel. Two crucial things to keep in mind:

  • If any task inside the nursery finishes with an unhandled exception, then the nursery immediately cancels all the tasks inside the nursery.

  • Since all of the tasks are running concurrently inside the async with block, the block does not exit until all tasks have completed. If you’ve used other concurrency frameworks, then you can think of it as, the de-indentation at the end of the async with automatically “joins” (waits for) all of the tasks in the nursery.

  • Once all the tasks have finished, then:

    • The nursery is marked as “closed”, meaning that no new tasks can be started inside it.

    • Any unhandled exceptions are re-raised inside the parent task. If there are multiple exceptions, then they’re collected up into a single MultiError exception.

Since all tasks are descendents of the initial task, one consequence of this is that run() can’t finish until all tasks have finished.

Note

A return statement will not cancel the nursery if it still has tasks running:

async def main():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(trio.sleep, 5)
        return

trio.run(main)

This code will wait 5 seconds (for the child task to finish), and then return.

Child tasks and cancellation

In Trio, child tasks inherit the parent nursery’s cancel scopes. So in this example, both the child tasks will be cancelled when the timeout expires:

with trio.move_on_after(TIMEOUT):
    async with trio.open_nursery() as nursery:
        nursery.start_soon(child1)
        nursery.start_soon(child2)

Note that what matters here is the scopes that were active when open_nursery() was called, not the scopes active when start_soon is called. So for example, the timeout block below does nothing at all:

async with trio.open_nursery() as nursery:
    with trio.move_on_after(TIMEOUT):  # don't do this!
        nursery.start_soon(child)

Why is this so? Well, start_soon() returns as soon as it has scheduled the new task to start running. The flow of execution in the parent then continues on to exit the with trio.move_on_after(TIMEOUT): block, at which point Trio forgets about the timeout entirely. In order for the timeout to apply to the child task, Trio must be able to tell that its associated cancel scope will stay open for at least as long as the child task is executing. And Trio can only know that for sure if the cancel scope block is outside the nursery block.

You might wonder why Trio can’t just remember “this task should be cancelled in TIMEOUT seconds”, even after the with trio.move_on_after(TIMEOUT): block is gone. The reason has to do with how cancellation is implemented. Recall that cancellation is represented by a Cancelled exception, which eventually needs to be caught by the cancel scope that caused it. (Otherwise, the exception would take down your whole program!) In order to be able to cancel the child tasks, the cancel scope has to be able to “see” the Cancelled exceptions that they raise – and those exceptions come out of the async with open_nursery() block, not out of the call to start_soon().

If you want a timeout to apply to one task but not another, then you need to put the cancel scope in that individual task’s function – child(), in this example.

Errors in multiple child tasks

Normally, in Python, only one thing happens at a time, which means that only one thing can wrong at a time. Trio has no such limitation. Consider code like:

async def broken1():
    d = {}
    return d["missing"]

async def broken2():
    seq = range(10)
    return seq[20]

async def parent():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(broken1)
        nursery.start_soon(broken2)

broken1 raises KeyError. broken2 raises IndexError. Obviously parent should raise some error, but what? In some sense, the answer should be “both of these at once”, but in Python there can only be one exception at a time.

Trio’s answer is that it raises a MultiError object. This is a special exception which encapsulates multiple exception objects – either regular exceptions or nested MultiErrors. To make these easier to work with, Trio installs a custom sys.excepthook that knows how to print nice tracebacks for unhandled MultiErrors, and it also provides some helpful utilities like MultiError.catch(), which allows you to catch “part of” a MultiError.

Spawning tasks without becoming a parent

Sometimes it doesn’t make sense for the task that starts a child to take on responsibility for watching it. For example, a server task may want to start a new task for each connection, but it can’t listen for connections and supervise children at the same time.

The solution here is simple once you see it: there’s no requirement that a nursery object stay in the task that created it! We can write code like this:

async def new_connection_listener(handler, nursery):
    while True:
        conn = await get_new_connection()
        nursery.start_soon(handler, conn)

async def server(handler):
    async with trio.open_nursery() as nursery:
        nursery.start_soon(new_connection_listener, handler, nursery)

Notice that server opens a nursery and passes it to new_connection_listener, and then new_connection_listener is able to start new tasks as “siblings” of itself. Of course, in this case, we could just as well have written:

async def server(handler):
    async with trio.open_nursery() as nursery:
        while True:
            conn = await get_new_connection()
            nursery.start_soon(handler, conn)

...but sometimes things aren’t so simple, and this trick comes in handy.

One thing to remember, though: cancel scopes are inherited from the nursery, not from the task that calls start_soon. So in this example, the timeout does not apply to child (or to anything else):

async def do_spawn(nursery):
    with trio.move_on_after(TIMEOUT):  # don't do this, it has no effect
        nursery.start_soon(child)

async with trio.open_nursery() as nursery:
    nursery.start_soon(do_spawn, nursery)

Custom supervisors

The default cleanup logic is often sufficient for simple cases, but what if you want a more sophisticated supervisor? For example, maybe you have Erlang envy and want features like automatic restart of crashed tasks. Trio itself doesn’t provide these kinds of features, but you can build them on top; Trio’s goal is to enforce basic hygiene and then get out of your way. (Specifically: Trio won’t let you build a supervisor that exits and leaves orphaned tasks behind, and if you have an unhandled exception due to bugs or laziness then Trio will make sure they propagate.) And then you can wrap your fancy supervisor up in a library and put it on PyPI, because supervisors are tricky and there’s no reason everyone should have to write their own.

For example, here’s a function that takes a list of functions, runs them all concurrently, and returns the result from the one that finishes first:

async def race(*async_fns):
    if not async_fns:
        raise ValueError("must pass at least one argument")

    winner = None

    async def jockey(async_fn, cancel_scope):
        nonlocal winner
        winner = await async_fn()
        cancel_scope.cancel()

    async with trio.open_nursery() as nursery:
        for async_fn in async_fns:
            nursery.start_soon(jockey, async_fn, nursery.cancel_scope)

    return winner

This works by starting a set of tasks which each try to run their function. As soon as the first function completes its execution, the task will set the nonlocal variable winner from the outer scope to the result of the function, and cancel the other tasks using the passed in cancel scope. Once all tasks have been cancelled (which exits the nursery block), the variable winner will be returned.

Here if one or more of the racing functions raises an unhandled exception then Trio’s normal handling kicks in: it cancels the others and then propagates the exception. If you want different behavior, you can get that by adding a try block to the jockey function to catch exceptions and handle them however you like.

Task-local storage

Suppose you’re writing a server that responds to network requests, and you log some information about each request as you process it. If the server is busy and there are multiple requests being handled at the same time, then you might end up with logs like this:

Request handler started
Request handler started
Request handler finished
Request handler finished

In this log, it’s hard to know which lines came from which request. (Did the request that started first also finish first, or not?) One way to solve this is to assign each request a unique identifier, and then include this identifier in each log message:

request 1: Request handler started
request 2: Request handler started
request 2: Request handler finished
request 1: Request handler finished

This way we can see that request 1 was slow: it started before request 2 but finished afterwards. (You can also get much fancier, but this is enough for an example.)

Now, here’s the problem: how does the logging code know what the request identifier is? One approach would be to explicitly pass it around to every function that might want to emit logs… but that’s basically every function, because you never know when you might need to add a log.debug(...) call to some utility function buried deep in the call stack, and when you’re in the middle of a debugging a nasty problem that last thing you want is to have to stop first and refactor everything to pass through the request identifier! Sometimes this is the right solution, but other times it would be much more convenient if we could store the identifier in a global variable, so that the logging function could look it up whenever it needed it. Except… a global variable can only have one value at a time, so if we have multiple handlers running at once then this isn’t going to work. What we need is something that’s like a global variable, but that can have different values depending on which request handler is accessing it.

To solve this problem, Python 3.7 added a new module to the standard library: contextvars. And not only does Trio have built-in support for contextvars, but if you’re using an earlier version of Python, then Trio makes sure that a backported version of contextvars is installed. So you can assume contextvars is there and works regardless of what version of Python you’re using.

Here’s a toy example demonstrating how to use contextvars:

import random
import trio
import contextvars

request_info = contextvars.ContextVar("request_info")


# Example logging function that tags each line with the request identifier.
def log(msg):
    # Read from task-local storage:
    request_tag = request_info.get()

    print(f"request {request_tag}: {msg}")


# An example "request handler" that does some work itself and also
# spawns some helper tasks to do some concurrent work.
async def handle_request(tag):
    # Write to task-local storage:
    request_info.set(tag)

    log("Request handler started")
    await trio.sleep(random.random())
    async with trio.open_nursery() as nursery:
        nursery.start_soon(concurrent_helper, "a")
        nursery.start_soon(concurrent_helper, "b")
    await trio.sleep(random.random())
    log("Request received finished")


async def concurrent_helper(job):
    log(f"Helper task {job} started")
    await trio.sleep(random.random())
    log(f"Helper task {job} finished")


# Spawn several "request handlers" simultaneously, to simulate a
# busy server handling multiple requests at the same time.
async def main():
    async with trio.open_nursery() as nursery:
        for i in range(3):
            nursery.start_soon(handle_request, i)


trio.run(main)

Example output (yours may differ slightly):

request 1: Request handler started
request 2: Request handler started
request 0: Request handler started
request 2: Helper task a started
request 2: Helper task b started
request 1: Helper task a started
request 1: Helper task b started
request 0: Helper task b started
request 0: Helper task a started
request 2: Helper task b finished
request 2: Helper task a finished
request 2: Request received finished
request 0: Helper task a finished
request 1: Helper task a finished
request 1: Helper task b finished
request 1: Request received finished
request 0: Helper task b finished
request 0: Request received finished

For more information, read the contextvars docs.

Synchronizing and communicating between tasks

Trio provides a standard set of synchronization and inter-task communication primitives. These objects’ APIs are generally modelled off of the analogous classes in the standard library, but with some differences.

Blocking and non-blocking methods

The standard library synchronization primitives have a variety of mechanisms for specifying timeouts and blocking behavior, and of signaling whether an operation returned due to success versus a timeout.

In Trio, we standardize on the following conventions:

  • We don’t provide timeout arguments. If you want a timeout, then use a cancel scope.

  • For operations that have a non-blocking variant, the blocking and non-blocking variants are different methods with names like X and X_nowait, respectively. (This is similar to queue.Queue, but unlike most of the classes in threading.) We like this approach because it allows us to make the blocking version async and the non-blocking version sync.

  • When a non-blocking method cannot succeed (the channel is empty, the lock is already held, etc.), then it raises trio.WouldBlock. There’s no equivalent to the queue.Empty versus queue.Full distinction – we just have the one exception that we use consistently.

Fairness

These classes are all guaranteed to be “fair”, meaning that when it comes time to choose who will be next to acquire a lock, get an item from a queue, etc., then it always goes to the task which has been waiting longest. It’s not entirely clear whether this is the best choice, but for now that’s how it works.

As an example of what this means, here’s a small program in which two tasks compete for a lock. Notice that the task which releases the lock always immediately attempts to re-acquire it, before the other task has a chance to run. (And remember that we’re doing cooperative multi-tasking here, so it’s actually deterministic that the task releasing the lock will call acquire() before the other task wakes up; in Trio releasing a lock is not a checkpoint.) With an unfair lock, this would result in the same task holding the lock forever and the other task being starved out. But if you run this, you’ll see that the two tasks politely take turns:

# fairness-demo.py

import trio

async def loopy_child(number, lock):
    while True:
        async with lock:
            print(f"Child {number} has the lock!")
            await trio.sleep(0.5)

async def main():
    async with trio.open_nursery() as nursery:
        lock = trio.Lock()
        nursery.start_soon(loopy_child, 1, lock)
        nursery.start_soon(loopy_child, 2, lock)

trio.run(main)

Broadcasting an event with Event

class trio.Event

A waitable boolean value useful for inter-task synchronization, inspired by threading.Event.

An event object has an internal boolean flag, representing whether the event has happened yet. The flag is initially False, and the wait() method waits until the flag is True. If the flag is already True, then wait() returns immediately. (If the event has already happened, there’s nothing to wait for.) The set() method sets the flag to True, and wakes up any waiters.

This behavior is useful because it helps avoid race conditions and lost wakeups: it doesn’t matter whether set() gets called just before or after wait(). If you want a lower-level wakeup primitive that doesn’t have this protection, consider Condition or trio.lowlevel.ParkingLot.

Note

Unlike threading.Event, trio.Event has no clear method. In Trio, once an Event has happened, it cannot un-happen. If you need to represent a series of events, consider creating a new Event object for each one (they’re cheap!), or other synchronization methods like channels or trio.lowlevel.ParkingLot.

is_set()

Return the current value of the internal flag.

set()

Set the internal flag value to True, and wake any waiting tasks.

statistics()

Return an object containing debugging information.

Currently the following fields are defined:

  • tasks_waiting: The number of tasks blocked on this event’s wait() method.

await wait()

Block until the internal flag value becomes True.

If it’s already True, then this method returns immediately.

Using channels to pass values between tasks

Channels allow you to safely and conveniently send objects between different tasks. They’re particularly useful for implementing producer/consumer patterns.

The core channel API is defined by the abstract base classes trio.abc.SendChannel and trio.abc.ReceiveChannel. You can use these to implement your own custom channels, that do things like pass objects between processes or over the network. But in many cases, you just want to pass objects between different tasks inside a single process, and for that you can use trio.open_memory_channel():

trio.open_memory_channel(max_buffer_size)

Open a channel for passing objects between tasks within a process.

Memory channels are lightweight, cheap to allocate, and entirely in-memory. They don’t involve any operating-system resources, or any kind of serialization. They just pass Python objects directly between tasks (with a possible stop in an internal buffer along the way).

Channel objects can be closed by calling aclose or using async with. They are not automatically closed when garbage collected. Closing memory channels isn’t mandatory, but it is generally a good idea, because it helps avoid situations where tasks get stuck waiting on a channel when there’s no-one on the other side. See Clean shutdown with channels for details.

Memory channel operations are all atomic with respect to cancellation, either receive will successfully return an object, or it will raise Cancelled while leaving the channel unchanged.

Parameters

max_buffer_size (int or math.inf) – The maximum number of items that can be buffered in the channel before send() blocks. Choosing a sensible value here is important to ensure that backpressure is communicated promptly and avoid unnecessary latency; see Buffering in channels for more details. If in doubt, use 0.

Returns

A pair (send_channel, receive_channel). If you have trouble remembering which order these go in, remember: data flows from left → right.

In addition to the standard channel methods, all memory channel objects provide a statistics() method, which returns an object with the following fields:

  • current_buffer_used: The number of items currently stored in the channel buffer.

  • max_buffer_size: The maximum number of items allowed in the buffer, as passed to open_memory_channel().

  • open_send_channels: The number of open MemorySendChannel endpoints pointing to this channel. Initially 1, but can be increased by MemorySendChannel.clone().

  • open_receive_channels: Likewise, but for open MemoryReceiveChannel endpoints.

  • tasks_waiting_send: The number of tasks blocked in send on this channel (summing over all clones).

  • tasks_waiting_receive: The number of tasks blocked in receive on this channel (summing over all clones).

Note

If you’ve used the threading or asyncio modules, you may be familiar with queue.Queue or asyncio.Queue. In Trio, open_memory_channel() is what you use when you’re looking for a queue. The main difference is that Trio splits the classic queue interface up into two objects. The advantage of this is that it makes it possible to put the two ends in different processes without rewriting your code, and that we can close the two sides separately.

MemorySendChannel and MemoryReceiveChannel also expose several more features beyond the core channel interface:

class trio.MemorySendChannel(*args, **kwargs)
clone()

Clone this send channel object.

This returns a new MemorySendChannel object, which acts as a duplicate of the original: sending on the new object does exactly the same thing as sending on the old object. (If you’re familiar with os.dup, then this is a similar idea.)

However, closing one of the objects does not close the other, and receivers don’t get EndOfChannel until all clones have been closed.

This is useful for communication patterns that involve multiple producers all sending objects to the same destination. If you give each producer its own clone of the MemorySendChannel, and then make sure to close each MemorySendChannel when it’s finished, receivers will automatically get notified when all producers are finished. See Managing multiple producers and/or multiple consumers for examples.

Raises

trio.ClosedResourceError – if you already closed this MemorySendChannel object.

close()

Close this send channel object synchronously.

All channel objects have an asynchronous aclose method. Memory channels can also be closed synchronously. This has the same effect on the channel and other tasks using it, but close is not a trio checkpoint. This simplifies cleaning up in cancelled tasks.

Using with send_channel: will close the channel object on leaving the with block.

await send(value)

See SendChannel.send.

Memory channels allow multiple tasks to call send at the same time.

send_nowait(value)

Like send, but if the channel’s buffer is full, raises WouldBlock instead of blocking.

class trio.MemoryReceiveChannel(*args, **kwargs)
clone()

Clone this receive channel object.

This returns a new MemoryReceiveChannel object, which acts as a duplicate of the original: receiving on the new object does exactly the same thing as receiving on the old object.

However, closing one of the objects does not close the other, and the underlying channel is not closed until all clones are closed. (If you’re familiar with os.dup, then this is a similar idea.)

This is useful for communication patterns that involve multiple consumers all receiving objects from the same underlying channel. See Managing multiple producers and/or multiple consumers for examples.

Warning

The clones all share the same underlying channel. Whenever a clone receive()s a value, it is removed from the channel and the other clones do not receive that value. If you want to send multiple copies of the same stream of values to multiple destinations, like itertools.tee(), then you need to find some other solution; this method does not do that.

Raises

trio.ClosedResourceError – if you already closed this MemoryReceiveChannel object.

close()

Close this receive channel object synchronously.

All channel objects have an asynchronous aclose method. Memory channels can also be closed synchronously. This has the same effect on the channel and other tasks using it, but close is not a trio checkpoint. This simplifies cleaning up in cancelled tasks.

Using with receive_channel: will close the channel object on leaving the with block.

await receive()

See ReceiveChannel.receive.

Memory channels allow multiple tasks to call receive at the same time. The first task will get the first item sent, the second task will get the second item sent, and so on.

receive_nowait()

Like receive, but if there’s nothing ready to receive, raises WouldBlock instead of blocking.

A simple channel example

Here’s a simple example of how to use memory channels:

import trio

async def main():
    async with trio.open_nursery() as nursery:
        # Open a channel:
        send_channel, receive_channel = trio.open_memory_channel(0)
        # Start a producer and a consumer, passing one end of the channel to
        # each of them:
        nursery.start_soon(producer, send_channel)
        nursery.start_soon(consumer, receive_channel)

async def producer(send_channel):
    # Producer sends 3 messages
    for i in range(3):
        # The producer sends using 'await send_channel.send(...)'
        await send_channel.send(f"message {i}")

async def consumer(receive_channel):
    # The consumer uses an 'async for' loop to receive the values:
    async for value in receive_channel:
        print(f"got value {value!r}")

trio.run(main)

If you run this, it prints:

got value "message 0"
got value "message 1"
got value "message 2"

And then it hangs forever. (Use control-C to quit.)

Clean shutdown with channels

Of course we don’t generally like it when programs hang. What happened? The problem is that the producer sent 3 messages and then exited, but the consumer has no way to tell that the producer is gone: for all it knows, another message might be coming along any moment. So it hangs forever waiting for the 4th message.

Here’s a new version that fixes this: it produces the same output as the previous version, and then exits cleanly. The only change is the addition of async with blocks inside the producer and consumer:

import trio

async def main():
    async with trio.open_nursery() as nursery:
        send_channel, receive_channel = trio.open_memory_channel(0)
        nursery.start_soon(producer, send_channel)
        nursery.start_soon(consumer, receive_channel)

async def producer(send_channel):
    async with send_channel:
        for i in range(3):
            await send_channel.send(f"message {i}")

async def consumer(receive_channel):
    async with receive_channel:
        async for value in receive_channel:
            print(f"got value {value!r}")

trio.run(main)

The really important thing here is the producer’s async with . When the producer exits, this closes the send_channel, and that tells the consumer that no more messages are coming, so it can cleanly exit its async for loop. Then the program shuts down because both tasks have exited.

We also added an async with to the consumer. This isn’t as important, but it can help us catch mistakes or other problems. For example, suppose that the consumer exited early for some reason – maybe because of a bug. Then the producer would be sending messages into the void, and might get stuck indefinitely. But, if the consumer closes its receive_channel, then the producer will get a BrokenResourceError to alert it that it should stop sending messages because no-one is listening.

If you want to see the effect of the consumer exiting early, try adding a break statement to the async for loop – you should see a BrokenResourceError from the producer.

Managing multiple producers and/or multiple consumers

You can also have multiple producers, and multiple consumers, all sharing the same channel. However, this makes shutdown a little more complicated.

For example, consider this naive extension of our previous example, now with two producers and two consumers:

# This example usually crashes!

import trio
import random

async def main():
    async with trio.open_nursery() as nursery:
        send_channel, receive_channel = trio.open_memory_channel(0)
        # Start two producers
        nursery.start_soon(producer, "A", send_channel)
        nursery.start_soon(producer, "B", send_channel)
        # And two consumers
        nursery.start_soon(consumer, "X", receive_channel)
        nursery.start_soon(consumer, "Y", receive_channel)

async def producer(name, send_channel):
    async with send_channel:
        for i in range(3):
            await send_channel.send(f"{i} from producer {name}")
            # Random sleeps help trigger the problem more reliably
            await trio.sleep(random.random())

async def consumer(name, receive_channel):
    async with receive_channel:
        async for value in receive_channel:
            print(f"consumer {name} got value {value!r}")
            # Random sleeps help trigger the problem more reliably
            await trio.sleep(random.random())

trio.run(main)

The two producers, A and B, send 3 messages apiece. These are then randomly distributed between the two consumers, X and Y. So we’re hoping to see some output like:

consumer Y got value '0 from producer B'
consumer X got value '0 from producer A'
consumer Y got value '1 from producer A'
consumer Y got value '1 from producer B'
consumer X got value '2 from producer B'
consumer X got value '2 from producer A'

However, on most runs, that’s not what happens – the first part of the output is OK, and then when we get to the end the program crashes with ClosedResourceError. If you run the program a few times, you’ll see that sometimes the traceback shows send crashing, and other times it shows receive crashing, and you might even find that on some runs it doesn’t crash at all.

Here’s what’s happening: suppose that producer A finishes first. It exits, and its async with block closes the send_channel. But wait! Producer B was still using that send_channel… so the next time B calls send, it gets a ClosedResourceError.

Sometimes, though if we’re lucky, the two producers might finish at the same time (or close enough), so they both make their last send before either of them closes the send_channel.

But, even if that happens, we’re not out of the woods yet! After the producers exit, the two consumers race to be the first to notice that the send_channel has closed. Suppose that X wins the race. It exits its async for loop, then exits the async with block… and closes the receive_channel, while Y is still using it. Again, this causes a crash.

We could avoid this by using some complicated bookkeeping to make sure that only the last producer and the last consumer close their channel endpoints… but that would be tiresome and fragile. Fortunately, there’s a better way! Here’s a fixed version of our program above:

import trio
import random

async def main():
    async with trio.open_nursery() as nursery:
        send_channel, receive_channel = trio.open_memory_channel(0)
        async with send_channel, receive_channel:
            # Start two producers, giving each its own private clone
            nursery.start_soon(producer, "A", send_channel.clone())
            nursery.start_soon(producer, "B", send_channel.clone())
            # And two consumers, giving each its own private clone
            nursery.start_soon(consumer, "X", receive_channel.clone())
            nursery.start_soon(consumer, "Y", receive_channel.clone())

async def producer(name, send_channel):
    async with send_channel:
        for i in range(3):
            await send_channel.send(f"{i} from producer {name}")
            # Random sleeps help trigger the problem more reliably
            await trio.sleep(random.random())

async def consumer(name, receive_channel):
    async with receive_channel:
        async for value in receive_channel:
            print(f"consumer {name} got value {value!r}")
            # Random sleeps help trigger the problem more reliably
            await trio.sleep(random.random())

trio.run(main)

This example demonstrates using the MemorySendChannel.clone and MemoryReceiveChannel.clone methods. What these do is create copies of our endpoints, that act just like the original – except that they can be closed independently. And the underlying channel is only closed after all the clones have been closed. So this completely solves our problem with shutdown, and if you run this program, you’ll see it print its six lines of output and then exits cleanly.

Notice a small trick we use: the code in main creates clone objects to pass into all the child tasks, and then closes the original objects using async with. Another option is to pass clones into all-but-one of the child tasks, and then pass the original object into the last task, like:

# Also works, but is more finicky:
send_channel, receive_channel = trio.open_memory_channel(0)
nursery.start_soon(producer, "A", send_channel.clone())
nursery.start_soon(producer, "B", send_channel)
nursery.start_soon(consumer, "X", receive_channel.clone())
nursery.start_soon(consumer, "Y", receive_channel)

But this is more error-prone, especially if you use a loop to spawn the producers/consumers.

Just make sure that you don’t write:

# Broken, will cause program to hang:
send_channel, receive_channel = trio.open_memory_channel(0)
nursery.start_soon(producer, "A", send_channel.clone())
nursery.start_soon(producer, "B", send_channel.clone())
nursery.start_soon(consumer, "X", receive_channel.clone())
nursery.start_soon(consumer, "Y", receive_channel.clone())

Here we pass clones into the tasks, but never close the original objects. That means we have 3 send channel objects (the original + two clones), but we only close 2 of them, so the consumers will hang around forever waiting for that last one to be closed.

Buffering in channels

When you call open_memory_channel(), you have to specify how many values can be buffered internally in the channel. If the buffer is full, then any task that calls send() will stop and wait for another task to call receive(). This is useful because it produces backpressure: if the channel producers are running faster than the consumers, then it forces the producers to slow down.

You can disable buffering entirely, by doing open_memory_channel(0). In that case any task that calls send() will wait until another task calls receive(), and vice versa. This is similar to how channels work in the classic Communicating Sequential Processes model, and is a reasonable default if you aren’t sure what size buffer to use. (That’s why we used it in the examples above.)

At the other extreme, you can make the buffer unbounded by using open_memory_channel(math.inf). In this case, send() always returns immediately. Normally, this is a bad idea. To see why, consider a program where the producer runs more quickly than the consumer:

# Simulate a producer that generates values 10x faster than the
# consumer can handle them.

import trio
import math

async def producer(send_channel):
    count = 0
    while True:
        # Pretend that we have to do some work to create this message, and it
        # takes 0.1 seconds:
        await trio.sleep(0.1)
        await send_channel.send(count)
        print("Sent message:", count)
        count += 1

async def consumer(receive_channel):
    async for value in receive_channel:
        print("Received message:", value)
        # Pretend that we have to do some work to handle this message, and it
        # takes 1 second
        await trio.sleep(1)

async def main():
    send_channel, receive_channel = trio.open_memory_channel(math.inf)
    async with trio.open_nursery() as nursery:
        nursery.start_soon(producer, send_channel)
        nursery.start_soon(consumer, receive_channel)

trio.run(main)

If you run this program, you’ll see output like:

Sent message: 0
Received message: 0
Sent message: 1
Sent message: 2
Sent message: 3
Sent message: 4
Sent message: 5
Sent message: 6
Sent message: 7
Sent message: 8
Sent message: 9
Received message: 1
Sent message: 10
Sent message: 11
Sent message: 12
...

On average, the producer sends ten messages per second, but the consumer only calls receive once per second. That means that each second, the channel’s internal buffer has to grow to hold an extra nine items. After a minute, the buffer will have ~540 items in it; after an hour, that grows to ~32,400. Eventually, the program will run out of memory. And well before we run out of memory, our latency on handling individual messages will become abysmal. For example, at the one minute mark, the producer is sending message ~600, but the consumer is still processing message ~60. Message 600 will have to sit in the channel for ~9 minutes before the consumer catches up and processes it.

Now try replacing open_memory_channel(math.inf) with open_memory_channel(0), and run it again. We get output like:

Sent message: 0
Received message: 0
Received message: 1
Sent message: 1
Received message: 2
Sent message: 2
Sent message: 3
Received message: 3
...

Now the send calls wait for the receive calls to finish, which forces the producer to slow down to match the consumer’s speed. (It might look strange that some values are reported as “Received” before they’re reported as “Sent”; this happens because the actual send/receive happen at the same time, so which line gets printed first is random.)

Now, let’s try setting a small but nonzero buffer size, like open_memory_channel(3). what do you think will happen?

I get:

Sent message: 0
Received message: 0
Sent message: 1
Sent message: 2
Sent message: 3
Received message: 1
Sent message: 4
Received message: 2
Sent message: 5
...

So you can see that the producer runs ahead by 3 messages, and then stops to wait: when the consumer reads message 1, it sends message 4, then when the consumer reads message 2, it sends message 5, and so on. Once it reaches the steady state, this version acts just like our previous version where we set the buffer size to 0, except that it uses a bit more memory and each message sits in the buffer for a bit longer before being processed (i.e., the message latency is higher).

Of course real producers and consumers are usually more complicated than this, and in some situations, a modest amount of buffering might improve throughput. But too much buffering wastes memory and increases latency, so if you want to tune your application you should experiment to see what value works best for you.

Why do we even support unbounded buffers then? Good question! Despite everything we saw above, there are times when you actually do need an unbounded buffer. For example, consider a web crawler that uses a channel to keep track of all the URLs it still wants to crawl. Each crawler runs a loop where it takes a URL from the channel, fetches it, checks the HTML for outgoing links, and then adds the new URLs to the channel. This creates a circular flow, where each consumer is also a producer. In this case, if your channel buffer gets full, then the crawlers will block when they try to add new URLs to the channel, and if all the crawlers got blocked, then they aren’t taking any URLs out of the channel, so they’re stuck forever in a deadlock. Using an unbounded channel avoids this, because it means that send() never blocks.

Lower-level synchronization primitives

Personally, I find that events and channels are usually enough to implement most things I care about, and lead to easier to read code than the lower-level primitives discussed in this section. But if you need them, they’re here. (If you find yourself reaching for these because you’re trying to implement a new higher-level synchronization primitive, then you might also want to check out the facilities in trio.lowlevel for a more direct exposure of Trio’s underlying synchronization logic. All of classes discussed in this section are implemented on top of the public APIs in trio.lowlevel; they don’t have any special access to Trio’s internals.)

class trio.CapacityLimiter(total_tokens)

An object for controlling access to a resource with limited capacity.

Sometimes you need to put a limit on how many tasks can do something at the same time. For example, you might want to use some threads to run multiple blocking I/O operations in parallel… but if you use too many threads at once, then your system can become overloaded and it’ll actually make things slower. One popular solution is to impose a policy like “run up to 40 threads at the same time, but no more”. But how do you implement a policy like this?

That’s what CapacityLimiter is for. You can think of a CapacityLimiter object as a sack that starts out holding some fixed number of tokens:

limit = trio.CapacityLimiter(40)

Then tasks can come along and borrow a token out of the sack:

# Borrow a token:
async with limit:
    # We are holding a token!
    await perform_expensive_operation()
# Exiting the 'async with' block puts the token back into the sack

And crucially, if you try to borrow a token but the sack is empty, then you have to wait for another task to finish what it’s doing and put its token back first before you can take it and continue.

Another way to think of it: a CapacityLimiter is like a sofa with a fixed number of seats, and if they’re all taken then you have to wait for someone to get up before you can sit down.

By default, trio.to_thread.run_sync() uses a CapacityLimiter to limit the number of threads running at once; see trio.to_thread.current_default_thread_limiter for details.

If you’re familiar with semaphores, then you can think of this as a restricted semaphore that’s specialized for one common use case, with additional error checking. For a more traditional semaphore, see Semaphore.

Note

Don’t confuse this with the “leaky bucket” or “token bucket” algorithms used to limit bandwidth usage on networks. The basic idea of using tokens to track a resource limit is similar, but this is a very simple sack where tokens aren’t automatically created or destroyed over time; they’re just borrowed and then put back.

await acquire()

Borrow a token from the sack, blocking if necessary.

Raises

RuntimeError – if the current task already holds one of this sack’s tokens.

acquire_nowait()

Borrow a token from the sack, without blocking.

Raises
  • WouldBlock – if no tokens are available.

  • RuntimeError – if the current task already holds one of this sack’s tokens.

await acquire_on_behalf_of(borrower)

Borrow a token from the sack on behalf of borrower, blocking if necessary.

Parameters

borrower – A trio.lowlevel.Task or arbitrary opaque object used to record who is borrowing this token; see acquire_on_behalf_of_nowait() for details.

Raises

RuntimeError – if borrower task already holds one of this sack’s tokens.

acquire_on_behalf_of_nowait(borrower)

Borrow a token from the sack on behalf of borrower, without blocking.

Parameters

borrower – A trio.lowlevel.Task or arbitrary opaque object used to record who is borrowing this token. This is used by trio.to_thread.run_sync() to allow threads to “hold tokens”, with the intention in the future of using it to allow deadlock detection and other useful things

Raises
  • WouldBlock – if no tokens are available.

  • RuntimeError – if borrower already holds one of this sack’s tokens.

available_tokens

The amount of capacity that’s available to use.

borrowed_tokens

The amount of capacity that’s currently in use.

release()

Put a token back into the sack.

Raises

RuntimeError – if the current task has not acquired one of this sack’s tokens.

release_on_behalf_of(borrower)

Put a token back into the sack on behalf of borrower.

Raises

RuntimeError – if the given borrower has not acquired one of this sack’s tokens.

statistics()

Return an object containing debugging information.

Currently the following fields are defined:

  • borrowed_tokens: The number of tokens currently borrowed from the sack.

  • total_tokens: The total number of tokens in the sack. Usually this will be larger than borrowed_tokens, but it’s possibly for it to be smaller if total_tokens was recently decreased.

  • borrowers: A list of all tasks or other entities that currently hold a token.

  • tasks_waiting: The number of tasks blocked on this CapacityLimiter’s acquire() or acquire_on_behalf_of() methods.

total_tokens

The total capacity available.

You can change total_tokens by assigning to this attribute. If you make it larger, then the appropriate number of waiting tasks will be woken immediately to take the new tokens. If you decrease total_tokens below the number of tasks that are currently using the resource, then all current tasks will be allowed to finish as normal, but no new tasks will be allowed in until the total number of tasks drops below the new total_tokens.

class trio.Semaphore(initial_value, *, max_value=None)

A semaphore.

A semaphore holds an integer value, which can be incremented by calling release() and decremented by calling acquire() – but the value is never allowed to drop below zero. If the value is zero, then acquire() will block until someone calls release().

If you’re looking for a Semaphore to limit the number of tasks that can access some resource simultaneously, then consider using a CapacityLimiter instead.

This object’s interface is similar to, but different from, that of threading.Semaphore.

A Semaphore object can be used as an async context manager; it blocks on entry but not on exit.

Parameters
  • initial_value (int) – A non-negative integer giving semaphore’s initial value.

  • max_value (int or None) – If given, makes this a “bounded” semaphore that raises an error if the value is about to exceed the given max_value.

await acquire()

Decrement the semaphore value, blocking if necessary to avoid letting it drop below zero.

acquire_nowait()

Attempt to decrement the semaphore value, without blocking.

Raises

WouldBlock – if the value is zero.

max_value

The maximum allowed value. May be None to indicate no limit.

release()

Increment the semaphore value, possibly waking a task blocked in acquire().

Raises

ValueError – if incrementing the value would cause it to exceed max_value.

statistics()

Return an object containing debugging information.

Currently the following fields are defined:

  • tasks_waiting: The number of tasks blocked on this semaphore’s acquire() method.

value

The current value of the semaphore.

class trio.Lock

A classic mutex.

This is a non-reentrant, single-owner lock. Unlike threading.Lock, only the owner of the lock is allowed to release it.

A Lock object can be used as an async context manager; it blocks on entry but not on exit.

await acquire()

Acquire the lock, blocking if necessary.

acquire_nowait()

Attempt to acquire the lock, without blocking.

Raises

WouldBlock – if the lock is held.

locked()

Check whether the lock is currently held.

Returns

True if the lock is held, False otherwise.

Return type

bool

release()

Release the lock.

Raises

RuntimeError – if the calling task does not hold the lock.

statistics()

Return an object containing debugging information.

Currently the following fields are defined:

  • locked: boolean indicating whether the lock is held.

  • owner: the trio.lowlevel.Task currently holding the lock, or None if the lock is not held.

  • tasks_waiting: The number of tasks blocked on this lock’s acquire() method.

class trio.StrictFIFOLock

A variant of Lock where tasks are guaranteed to acquire the lock in strict first-come-first-served order.

An example of when this is useful is if you’re implementing something like trio.SSLStream or an HTTP/2 server using h2, where you have multiple concurrent tasks that are interacting with a shared state machine, and at unpredictable moments the state machine requests that a chunk of data be sent over the network. (For example, when using h2 simply reading incoming data can occasionally create outgoing data to send.) The challenge is to make sure that these chunks are sent in the correct order, without being garbled.

One option would be to use a regular Lock, and wrap it around every interaction with the state machine:

# This approach is sometimes workable but often sub-optimal; see below
async with lock:
    state_machine.do_something()
    if state_machine.has_data_to_send():
        await conn.sendall(state_machine.get_data_to_send())

But this can be problematic. If you’re using h2 then usually reading incoming data doesn’t create the need to send any data, so we don’t want to force every task that tries to read from the network to sit and wait a potentially long time for sendall to finish. And in some situations this could even potentially cause a deadlock, if the remote peer is waiting for you to read some data before it accepts the data you’re sending.

StrictFIFOLock provides an alternative. We can rewrite our example like:

# Note: no awaits between when we start using the state machine and
# when we block to take the lock!
state_machine.do_something()
if state_machine.has_data_to_send():
    # Notice that we fetch the data to send out of the state machine
    # *before* sleeping, so that other tasks won't see it.
    chunk = state_machine.get_data_to_send()
    async with strict_fifo_lock:
        await conn.sendall(chunk)

First we do all our interaction with the state machine in a single scheduling quantum (notice there are no awaits in there), so it’s automatically atomic with respect to other tasks. And then if and only if we have data to send, we get in line to send it – and StrictFIFOLock guarantees that each task will send its data in the same order that the state machine generated it.

Currently, StrictFIFOLock is identical to Lock, but (a) this may not always be true in the future, especially if Trio ever implements more sophisticated scheduling policies, and (b) the above code is relying on a pretty subtle property of its lock. Using a StrictFIFOLock acts as an executable reminder that you’re relying on this property.

class trio.Condition(lock=None)

A classic condition variable, similar to threading.Condition.

A Condition object can be used as an async context manager to acquire the underlying lock; it blocks on entry but not on exit.

Parameters

lock (Lock) – the lock object to use. If given, must be a trio.Lock. If None, a new Lock will be allocated and used.

await acquire()

Acquire the underlying lock, blocking if necessary.

acquire_nowait()

Attempt to acquire the underlying lock, without blocking.

Raises

WouldBlock – if the lock is currently held.

locked()

Check whether the underlying lock is currently held.

Returns

True if the lock is held, False otherwise.

Return type

bool

notify(n=1)

Wake one or more tasks that are blocked in wait().

Parameters

n (int) – The number of tasks to wake.

Raises

RuntimeError – if the calling task does not hold the lock.

notify_all()

Wake all tasks that are currently blocked in wait().

Raises

RuntimeError – if the calling task does not hold the lock.

release()

Release the underlying lock.

statistics()

Return an object containing debugging information.

Currently the following fields are defined:

  • tasks_waiting: The number of tasks blocked on this condition’s wait() method.

  • lock_statistics: The result of calling the underlying Locks statistics() method.

await wait()

Wait for another task to call notify() or notify_all().

When calling this method, you must hold the lock. It releases the lock while waiting, and then re-acquires it before waking up.

There is a subtlety with how this method interacts with cancellation: when cancelled it will block to re-acquire the lock before raising Cancelled. This may cause cancellation to be less prompt than expected. The advantage is that it makes code like this work:

async with condition:
    await condition.wait()

If we didn’t re-acquire the lock before waking up, and wait() were cancelled here, then we’d crash in condition.__aexit__ when we tried to release the lock we no longer held.

Raises

RuntimeError – if the calling task does not hold the lock.

Notes on async generators

Python 3.6 added support for async generators, which can use await, async for, and async with in between their yield statements. As you might expect, you use async for to iterate over them. PEP 525 has many more details if you want them.

For example, the following is a roundabout way to print the numbers 0 through 9 with a 1-second delay before each one:

async def range_slowly(*args):
    """Like range(), but adds a 1-second sleep before each value."""
    for value in range(*args):
        await trio.sleep(1)
        yield value

async def use_it():
    async for value in range_slowly(10):
        print(value)

trio.run(use_it)

Trio supports async generators, with some caveats described in this section.

Finalization

If you iterate over an async generator in its entirety, like the example above does, then the execution of the async generator will occur completely in the context of the code that’s iterating over it, and there aren’t too many surprises.

If you abandon a partially-completed async generator, though, such as by breaking out of the iteration, things aren’t so simple. The async generator iterator object is still alive, waiting for you to resume iterating it so it can produce more values. At some point, Python will realize that you’ve dropped all references to the iterator, and will call on Trio to throw in a GeneratorExit exception so that any remaining cleanup code inside the generator has a chance to run: finally blocks, __aexit__ handlers, and so on.

So far, so good. Unfortunately, Python provides no guarantees about when this happens. It could be as soon as you break out of the async for loop, or an arbitrary amount of time later. It could even be after the entire Trio run has finished! Just about the only guarantee is that it won’t happen in the task that was using the generator. That task will continue on with whatever else it’s doing, and the async generator cleanup will happen “sometime later, somewhere else”: potentially with different context variables, not subject to timeouts, and/or after any nurseries you’re using have been closed.

If you don’t like that ambiguity, and you want to ensure that a generator’s finally blocks and __aexit__ handlers execute as soon as you’re done using it, then you’ll need to wrap your use of the generator in something like async_generator.aclosing():

# Instead of this:
async for value in my_generator():
    if value == 42:
        break

# Do this:
async with aclosing(my_generator()) as aiter:
    async for value in aiter:
        if value == 42:
            break

This is cumbersome, but Python unfortunately doesn’t provide any other reliable options. If you use aclosing(), then your generator’s cleanup code executes in the same context as the rest of its iterations, so timeouts, exceptions, and context variables work like you’d expect.

If you don’t use aclosing(), then Trio will do its best anyway, but you’ll have to contend with the following semantics:

  • The cleanup of the generator occurs in a cancelled context, i.e., all blocking calls executed during cleanup will raise Cancelled. This is to compensate for the fact that any timeouts surrounding the original use of the generator have been long since forgotten.

  • The cleanup runs without access to any context variables that may have been present when the generator was originally being used.

  • If the generator raises an exception during cleanup, then it’s printed to the trio.async_generator_errors logger and otherwise ignored.

  • If an async generator is still alive at the end of the whole call to trio.run(), then it will be cleaned up after all tasks have exited and before trio.run() returns. Since the “system nursery” has already been closed at this point, Trio isn’t able to support any new calls to trio.lowlevel.spawn_system_task().

If you plan to run your code on PyPy to take advantage of its better performance, you should be aware that PyPy is far more likely than CPython to perform async generator cleanup at a time well after the last use of the generator. (This is a consequence of the fact that PyPy does not use reference counting to manage memory.) To help catch issues like this, Trio will issue a ResourceWarning (ignored by default, but enabled when running under python -X dev for example) for each async generator that needs to be handled through the fallback finalization path.

Cancel scopes and nurseries

Warning

You may not write a yield statement that suspends an async generator inside a CancelScope or Nursery that was entered within the generator.

That is, this is OK:

async def some_agen():
    with trio.move_on_after(1):
        await long_operation()
    yield "first"
    async with trio.open_nursery() as nursery:
        nursery.start_soon(task1)
        nursery.start_soon(task2)
    yield "second"
    ...

But this is not:

async def some_agen():
    with trio.move_on_after(1):
        yield "first"
    async with trio.open_nursery() as nursery:
        yield "second"
    ...

Async generators decorated with @asynccontextmanager to serve as the template for an async context manager are not subject to this constraint, because @asynccontextmanager uses them in a limited way that doesn’t create problems.

Violating the rule described in this section will sometimes get you a useful error message, but Trio is not able to detect all such cases, so sometimes you’ll get an unhelpful TrioInternalError. (And sometimes it will seem to work, which is probably the worst outcome of all, since then you might not notice the issue until you perform some minor refactoring of the generator or the code that’s iterating it, or just get unlucky. There is a proposed Python enhancement that would at least make it fail consistently.)

The reason for the restriction on cancel scopes has to do with the difficulty of noticing when a generator gets suspended and resumed. The cancel scopes inside the generator shouldn’t affect code running outside the generator, but Trio isn’t involved in the process of exiting and reentering the generator, so it would be hard pressed to keep its cancellation plumbing in the correct state. Nurseries use a cancel scope internally, so they have all the problems of cancel scopes plus a number of problems of their own: for example, when the generator is suspended, what should the background tasks do? There’s no good way to suspend them, but if they keep running and throw an exception, where can that exception be reraised?

If you have an async generator that wants to yield from within a nursery or cancel scope, your best bet is to refactor it to be a separate task that communicates over memory channels. The trio_util package offers a decorator that does this for you transparently.

For more discussion, see Trio issues 264 (especially this comment) and 638.

Threads (if you must)

In a perfect world, all third-party libraries and low-level APIs would be natively async and integrated into Trio, and all would be happiness and rainbows.

That world, alas, does not (yet) exist. Until it does, you may find yourself needing to interact with non-Trio APIs that do rude things like “blocking”.

In acknowledgment of this reality, Trio provides two useful utilities for working with real, operating-system level, threading-module-style threads. First, if you’re in Trio but need to push some blocking I/O into a thread, there’s trio.to_thread.run_sync. And if you’re in a thread and need to communicate back with Trio, you can use trio.from_thread.run() and trio.from_thread.run_sync().

Trio’s philosophy about managing worker threads

If you’ve used other I/O frameworks, you may have encountered the concept of a “thread pool”, which is most commonly implemented as a fixed size collection of threads that hang around waiting for jobs to be assigned to them. These solve two different problems: First, re-using the same threads over and over is more efficient than starting and stopping a new thread for every job you need done; basically, the pool acts as a kind of cache for idle threads. And second, having a fixed size avoids getting into a situation where 100,000 jobs are submitted simultaneously, and then 100,000 threads are spawned and the system gets overloaded and crashes. Instead, the N threads start executing the first N jobs, while the other (100,000 - N) jobs sit in a queue and wait their turn. Which is generally what you want, and this is how trio.to_thread.run_sync() works by default.

The downside of this kind of thread pool is that sometimes, you need more sophisticated logic for controlling how many threads are run at once. For example, you might want a policy like “at most 20 threads total, but no more than 3 of those can be running jobs associated with the same user account”, or you might want a pool whose size is dynamically adjusted over time in response to system conditions.

It’s even possible for a fixed-size policy to cause unexpected deadlocks. Imagine a situation where we have two different types of blocking jobs that you want to run in the thread pool, type A and type B. Type A is pretty simple: it just runs and completes pretty quickly. But type B is more complicated: it has to stop in the middle and wait for some other work to finish, and that other work includes running a type A job. Now, suppose you submit N jobs of type B to the pool. They all start running, and then eventually end up submitting one or more jobs of type A. But since every thread in our pool is already busy, the type A jobs don’t actually start running – they just sit in a queue waiting for the type B jobs to finish. But the type B jobs will never finish, because they’re waiting for the type A jobs. Our system has deadlocked. The ideal solution to this problem is to avoid having type B jobs in the first place – generally it’s better to keep complex synchronization logic in the main Trio thread. But if you can’t do that, then you need a custom thread allocation policy that tracks separate limits for different types of jobs, and make it impossible for type B jobs to fill up all the slots that type A jobs need to run.

So, we can see that it’s important to be able to change the policy controlling the allocation of threads to jobs. But in many frameworks, this requires implementing a new thread pool from scratch, which is highly non-trivial; and if different types of jobs need different policies, then you may have to create multiple pools, which is inefficient because now you effectively have two different thread caches that aren’t sharing resources.

Trio’s solution to this problem is to split worker thread management into two layers. The lower layer is responsible for taking blocking I/O jobs and arranging for them to run immediately on some worker thread. It takes care of solving the tricky concurrency problems involved in managing threads and is responsible for optimizations like re-using threads, but has no admission control policy: if you give it 100,000 jobs, it will spawn 100,000 threads. The upper layer is responsible for providing the policy to make sure that this doesn’t happen – but since it only has to worry about policy, it can be much simpler. In fact, all there is to it is the limiter= argument passed to trio.to_thread.run_sync(). This defaults to a global CapacityLimiter object, which gives us the classic fixed-size thread pool behavior. (See trio.to_thread.current_default_thread_limiter().) But if you want to use “separate pools” for type A jobs and type B jobs, then it’s just a matter of creating two separate CapacityLimiter objects and passing them in when running these jobs. Or here’s an example of defining a custom policy that respects the global thread limit, while making sure that no individual user can use more than 3 threads at a time:

class CombinedLimiter:
     def __init__(self, first, second):
         self._first = first
         self._second = second

     async def acquire_on_behalf_of(self, borrower):
         # Acquire both, being careful to clean up properly on error
         await self._first.acquire_on_behalf_of(borrower)
         try:
             await self._second.acquire_on_behalf_of(borrower)
         except:
             self._first.release_on_behalf_of(borrower)
             raise

     def release_on_behalf_of(self, borrower):
         # Release both, being careful to clean up properly on error
         try:
             self._second.release_on_behalf_of(borrower)
         finally:
             self._first.release_on_behalf_of(borrower)


# Use a weak value dictionary, so that we don't waste memory holding
# limiter objects for users who don't have any worker threads running.
USER_LIMITERS = weakref.WeakValueDictionary()
MAX_THREADS_PER_USER = 3

def get_user_limiter(user_id):
    try:
        return USER_LIMITERS[user_id]
    except KeyError:
        per_user_limiter = trio.CapacityLimiter(MAX_THREADS_PER_USER)
        global_limiter = trio.current_default_thread_limiter()
        # IMPORTANT: acquire the per_user_limiter before the global_limiter.
        # If we get 100 jobs for a user at the same time, we want
        # to only allow 3 of them at a time to even compete for the
        # global thread slots.
        combined_limiter = CombinedLimiter(per_user_limiter, global_limiter)
        USER_LIMITERS[user_id] = combined_limiter
        return combined_limiter


async def run_sync_in_thread_for_user(user_id, sync_fn, *args):
    combined_limiter = get_user_limiter(user_id)
    return await trio.to_thread.run_sync(sync_fn, *args, limiter=combined_limiter)

Putting blocking I/O into worker threads

await trio.to_thread.run_sync(sync_fn, *args, cancellable=False, limiter=None)

Convert a blocking operation into an async operation using a thread.

These two lines are equivalent:

sync_fn(*args)
await trio.to_thread.run_sync(sync_fn, *args)

except that if sync_fn takes a long time, then the first line will block the Trio loop while it runs, while the second line allows other Trio tasks to continue working while sync_fn runs. This is accomplished by pushing the call to sync_fn(*args) off into a worker thread.

From inside the worker thread, you can get back into Trio using the functions in trio.from_thread.

Parameters
  • sync_fn – An arbitrary synchronous callable.

  • *args – Positional arguments to pass to sync_fn. If you need keyword arguments, use functools.partial().

  • cancellable (bool) – Whether to allow cancellation of this operation. See discussion below.

  • limiter (None, or CapacityLimiter-like object) –

    An object used to limit the number of simultaneous threads. Most commonly this will be a CapacityLimiter, but it could be anything providing compatible acquire_on_behalf_of() and release_on_behalf_of() methods. This function will call acquire_on_behalf_of before starting the thread, and release_on_behalf_of after the thread has finished.

    If None (the default), uses the default CapacityLimiter, as returned by current_default_thread_limiter().

Cancellation handling: Cancellation is a tricky issue here, because neither Python nor the operating systems it runs on provide any general mechanism for cancelling an arbitrary synchronous function running in a thread. This function will always check for cancellation on entry, before starting the thread. But once the thread is running, there are two ways it can handle being cancelled:

  • If cancellable=False, the function ignores the cancellation and keeps going, just like if we had called sync_fn synchronously. This is the default behavior.

  • If cancellable=True, then this function immediately raises Cancelled. In this case the thread keeps running in background – we just abandon it to do whatever it’s going to do, and silently discard any return value or errors that it raises. Only use this if you know that the operation is safe and side-effect free. (For example: trio.socket.getaddrinfo() uses a thread with cancellable=True, because it doesn’t really affect anything if a stray hostname lookup keeps running in the background.)

    The limiter is only released after the thread has actually finished – which in the case of cancellation may be some time after this function has returned. If trio.run() finishes before the thread does, then the limiter release method will never be called at all.

Warning

You should not use this function to call long-running CPU-bound functions! In addition to the usual GIL-related reasons why using threads for CPU-bound work is not very effective in Python, there is an additional problem: on CPython, CPU-bound threads tend to “starve out” IO-bound threads, so using threads for CPU-bound work is likely to adversely affect the main thread running Trio. If you need to do this, you’re better off using a worker process, or perhaps PyPy (which still has a GIL, but may do a better job of fairly allocating CPU time between threads).

Returns

Whatever sync_fn(*args) returns.

Raises

Exception – Whatever sync_fn(*args) raises.

trio.to_thread.current_default_thread_limiter()

Get the default CapacityLimiter used by trio.to_thread.run_sync.

The most common reason to call this would be if you want to modify its total_tokens attribute.

Getting back into the Trio thread from another thread

trio.from_thread.run(afn, *args, trio_token=None)

Run the given async function in the parent Trio thread, blocking until it is complete.

Returns

Whatever afn(*args) returns.

Returns or raises whatever the given function returns or raises. It can also raise exceptions of its own:

Raises
  • RunFinishedError – if the corresponding call to trio.run() has already completed, or if the run has started its final cleanup phase and can no longer spawn new system tasks.

  • Cancelled – if the corresponding call to trio.run() completes while afn(*args) is running, then afn is likely to raise trio.Cancelled, and this will propagate out into

  • RuntimeError – if you try calling this from inside the Trio thread, which would otherwise cause a deadlock.

  • AttributeError – if no trio_token was provided, and we can’t infer one from context.

  • TypeError – if afn is not an asynchronous function.

Locating a Trio Token: There are two ways to specify which trio.run loop to reenter:

  • Spawn this thread from trio.to_thread.run_sync. Trio will automatically capture the relevant Trio token and use it when you want to re-enter Trio.

  • Pass a keyword argument, trio_token specifying a specific trio.run loop to re-enter. This is useful in case you have a “foreign” thread, spawned using some other framework, and still want to enter Trio.

trio.from_thread.run_sync(fn, *args, trio_token=None)

Run the given sync function in the parent Trio thread, blocking until it is complete.

Returns

Whatever fn(*args) returns.

Returns or raises whatever the given function returns or raises. It can also raise exceptions of its own:

Raises
  • RunFinishedError – if the corresponding call to trio.run has already completed.

  • RuntimeError – if you try calling this from inside the Trio thread, which would otherwise cause a deadlock.

  • AttributeError – if no trio_token was provided, and we can’t infer one from context.

  • TypeError – if fn is an async function.

Locating a Trio Token: There are two ways to specify which trio.run loop to reenter:

  • Spawn this thread from trio.to_thread.run_sync. Trio will automatically capture the relevant Trio token and use it when you want to re-enter Trio.

  • Pass a keyword argument, trio_token specifying a specific trio.run loop to re-enter. This is useful in case you have a “foreign” thread, spawned using some other framework, and still want to enter Trio.

This will probably be clearer with an example. Here we demonstrate how to spawn a child thread, and then use a memory channel to send messages between the thread and a Trio task:

import trio


def thread_fn(receive_from_trio, send_to_trio):
    while True:
        # Since we're in a thread, we can't call methods on Trio
        # objects directly -- so we use trio.from_thread to call them.
        try:
            request = trio.from_thread.run(receive_from_trio.receive)
        except trio.EndOfChannel:
            trio.from_thread.run(send_to_trio.aclose)
            return
        else:
            response = request + 1
            trio.from_thread.run(send_to_trio.send, response)


async def main():
    send_to_thread, receive_from_trio = trio.open_memory_channel(0)
    send_to_trio, receive_from_thread = trio.open_memory_channel(0)

    async with trio.open_nursery() as nursery:
        # In a background thread, run:
        #   thread_fn(receive_from_trio, send_to_trio)
        nursery.start_soon(
            trio.to_thread.run_sync, thread_fn, receive_from_trio, send_to_trio
        )

        # prints "1"
        await send_to_thread.send(0)
        print(await receive_from_thread.receive())

        # prints "2"
        await send_to_thread.send(1)
        print(await receive_from_thread.receive())

        # When we close the channel, it signals the thread to exit.
        await send_to_thread.aclose()

        # When we exit the nursery, it waits for the background thread to
        # exit.


trio.run(main)

Threads and task-local storage

When working with threads, you can use the same contextvars we discussed above, because their values are preserved.

This is done by automatically copying the contextvars context when you use any of:

That means that the values of the context variables are accessible even in worker threads, or when sending a function to be run in the main/parent Trio thread using trio.from_thread.run from one of these worker threads.

But it also means that as the context is not the same but a copy, if you set the context variable value inside one of these functions that work in threads, the new value will only be available in that context (that was copied). So, the new value will be available for that function and other internal/children tasks, but the value won’t be available in the parent thread.

If you need to modify values that would live in the context variables and you need to make those modifications from the child threads, you can instead set a mutable object (e.g. a dictionary) in the context variable of the top level/parent Trio thread. Then in the children, instead of setting the context variable, you can get the same object, and modify its values. That way you keep the same object in the context variable and only mutate it in child threads.

This way, you can modify the object content in child threads and still access the new content in the parent thread.

Here’s an example:

import contextvars
import time

import trio

request_state = contextvars.ContextVar("request_state")

# Blocking function that should be run on a thread
# It could be reading or writing files, communicating with a database
# with a driver not compatible with async / await, etc.
def work_in_thread(msg):
    # Only use request_state.get() inside the worker thread
    state_value = request_state.get()
    current_user_id = state_value["current_user_id"]
    time.sleep(3)  # this would be some blocking call, like reading a file
    print(f"Processed user {current_user_id} with message {msg} in a thread worker")
    # Modify/mutate the state object, without setting the entire
    # contextvar with request_state.set()
    state_value["msg"] = msg


# An example "request handler" that does some work itself and also
# spawns some helper tasks in threads to execute blocking code.
async def handle_request(current_user_id):
    # Write to task-local storage:
    current_state = {"current_user_id": current_user_id, "msg": ""}
    request_state.set(current_state)

    # Here the current implicit contextvars context will be automatically copied
    # inside the worker thread
    await trio.to_thread.run_sync(work_in_thread, f"Hello {current_user_id}")
    # Extract the value set inside the thread in the same object stored in a contextvar
    new_msg = current_state["msg"]
    print(
        f"New contextvar value from worker thread for user {current_user_id}: {new_msg}"
    )


# Spawn several "request handlers" simultaneously, to simulate a
# busy server handling multiple requests at the same time.
async def main():
    async with trio.open_nursery() as nursery:
        for i in range(3):
            nursery.start_soon(handle_request, i)


trio.run(main)

Running that script will result in the output:

Processed user 2 with message Hello 2 in a thread worker
Processed user 0 with message Hello 0 in a thread worker
Processed user 1 with message Hello 1 in a thread worker
New contextvar value from worker thread for user 2: Hello 2
New contextvar value from worker thread for user 1: Hello 1
New contextvar value from worker thread for user 0: Hello 0

If you are using contextvars or you are using a library that uses them, now you know how they interact when working with threads in Trio.

But have in mind that in many cases it might be a lot simpler to not use context variables in your own code and instead pass values in arguments, as it might be more explicit and might be easier to reason about.

Note

The context is automatically copied instead of using the same parent context because a single context can’t be used in more than one thread, it’s not supported by contextvars.

Exceptions and warnings

exception trio.Cancelled(*args, **kwargs)

Raised by blocking calls if the surrounding scope has been cancelled.

You should let this exception propagate, to be caught by the relevant cancel scope. To remind you of this, it inherits from BaseException instead of Exception, just like KeyboardInterrupt and SystemExit do. This means that if you write something like:

try:
    ...
except Exception:
    ...

then this won’t catch a Cancelled exception.

You cannot raise Cancelled yourself. Attempting to do so will produce a TypeError. Use cancel_scope.cancel() instead.

Note

In the US it’s also common to see this word spelled “canceled”, with only one “l”. This is a recent and US-specific innovation, and even in the US both forms are still commonly used. So for consistency with the rest of the world and with “cancellation” (which always has two “l”s), Trio uses the two “l” spelling everywhere.

exception trio.TooSlowError

Raised by fail_after() and fail_at() if the timeout expires.

exception trio.WouldBlock

Raised by X_nowait functions if X would block.

exception trio.EndOfChannel

Raised when trying to receive from a trio.abc.ReceiveChannel that has no more data to receive.

This is analogous to an “end-of-file” condition, but for channels.

exception trio.BusyResourceError

Raised when a task attempts to use a resource that some other task is already using, and this would lead to bugs and nonsense.

For example, if two tasks try to send data through the same socket at the same time, Trio will raise BusyResourceError instead of letting the data get scrambled.

exception trio.ClosedResourceError

Raised when attempting to use a resource after it has been closed.

Note that “closed” here means that your code closed the resource, generally by calling a method with a name like close or aclose, or by exiting a context manager. If a problem arises elsewhere – for example, because of a network failure, or because a remote peer closed their end of a connection – then that should be indicated by a different exception class, like BrokenResourceError or an OSError subclass.

exception trio.BrokenResourceError

Raised when an attempt to use a resource fails due to external circumstances.

For example, you might get this if you try to send data on a stream where the remote side has already closed the connection.

You don’t get this error if you closed the resource – in that case you get ClosedResourceError.

This exception’s __cause__ attribute will often contain more information about the underlying error.

exception trio.RunFinishedError

Raised by trio.from_thread.run and similar functions if the corresponding call to trio.run() has already finished.

exception trio.TrioInternalError

Raised by run() if we encounter a bug in Trio, or (possibly) a misuse of one of the low-level trio.lowlevel APIs.

This should never happen! If you get this error, please file a bug.

Unfortunately, if you get this error it also means that all bets are off – Trio doesn’t know what is going on and its normal invariants may be void. (For example, we might have “lost track” of a task. Or lost track of all tasks.) Again, though, this shouldn’t happen.

exception trio.TrioDeprecationWarning

Bases: FutureWarning

Warning emitted if you use deprecated Trio functionality.

As a young project, Trio is currently quite aggressive about deprecating and/or removing functionality that we realize was a bad idea. If you use Trio, you should subscribe to issue #1 to get information about upcoming deprecations and other backwards compatibility breaking changes.

Despite the name, this class currently inherits from FutureWarning, not DeprecationWarning, because while we’re in young-and-aggressive mode we want these warnings to be visible by default. You can hide them by installing a filter or with the -W switch: see the warnings documentation for details.

I/O in Trio

The abstract Stream API

Trio provides a set of abstract base classes that define a standard interface for unidirectional and bidirectional byte streams.

Why is this useful? Because it lets you write generic protocol implementations that can work over arbitrary transports, and easily create complex transport configurations. Here’s some examples:

  • trio.SocketStream wraps a raw socket (like a TCP connection over the network), and converts it to the standard stream interface.

  • trio.SSLStream is a “stream adapter” that can take any object that implements the trio.abc.Stream interface, and convert it into an encrypted stream. In Trio the standard way to speak SSL over the network is to wrap an SSLStream around a SocketStream.

  • If you spawn a subprocess, you can get a SendStream that lets you write to its stdin, and a ReceiveStream that lets you read from its stdout. If for some reason you wanted to speak SSL to a subprocess, you could use a StapledStream to combine its stdin/stdout into a single bidirectional Stream, and then wrap that in an SSLStream:

    ssl_context = ssl.create_default_context()
    ssl_context.check_hostname = False
    s = SSLStream(StapledStream(process.stdin, process.stdout), ssl_context)
    
  • It sometimes happens that you want to connect to an HTTPS server, but you have to go through a web proxy… and the proxy also uses HTTPS. So you end up having to do SSL-on-top-of-SSL. In Trio this is trivial – just wrap your first SSLStream in a second SSLStream:

    # Get a raw SocketStream connection to the proxy:
    s0 = await open_tcp_stream("proxy", 443)
    
    # Set up SSL connection to proxy:
    s1 = SSLStream(s0, proxy_ssl_context, server_hostname="proxy")
    # Request a connection to the website
    await s1.send_all(b"CONNECT website:443 / HTTP/1.0\r\n\r\n")
    await check_CONNECT_response(s1)
    
    # Set up SSL connection to the real website. Notice that s1 is
    # already an SSLStream object, and here we're wrapping a second
    # SSLStream object around it.
    s2 = SSLStream(s1, website_ssl_context, server_hostname="website")
    # Make our request
    await s2.send_all(b"GET /index.html HTTP/1.0\r\n\r\n")
    ...
    
  • The trio.testing module provides a set of flexible in-memory stream object implementations, so if you have a protocol implementation to test then you can can start two tasks, set up a virtual “socket” connecting them, and then do things like inject random-but-repeatable delays into the connection.

Abstract base classes

Overview: abstract base classes for I/O

Abstract base class

Inherits from…

Adds these abstract methods…

And these concrete methods.

Example implementations

AsyncResource

aclose()

__aenter__, __aexit__

Asynchronous file objects

SendStream

AsyncResource

send_all(), wait_send_all_might_not_block()

MemorySendStream

ReceiveStream

AsyncResource

receive_some()

__aiter__, __anext__

MemoryReceiveStream

Stream

SendStream, ReceiveStream

SSLStream

HalfCloseableStream

Stream

send_eof()

SocketStream, StapledStream

Listener

AsyncResource

accept()

SocketListener, SSLListener

SendChannel

AsyncResource

send()

MemorySendChannel

ReceiveChannel

AsyncResource

receive()

__aiter__, __anext__

MemoryReceiveChannel

Channel

SendChannel, ReceiveChannel

class trio.abc.AsyncResource

A standard interface for resources that needs to be cleaned up, and where that cleanup may require blocking operations.

This class distinguishes between “graceful” closes, which may perform I/O and thus block, and a “forceful” close, which cannot. For example, cleanly shutting down a TLS-encrypted connection requires sending a “goodbye” message; but if a peer has become non-responsive, then sending this message might block forever, so we may want to just drop the connection instead. Therefore the aclose() method is unusual in that it should always close the connection (or at least make its best attempt) even if it fails; failure indicates a failure to achieve grace, not a failure to close the connection.

Objects that implement this interface can be used as async context managers, i.e., you can write:

async with create_resource() as some_async_resource:
    ...

Entering the context manager is synchronous (not a checkpoint); exiting it calls aclose(). The default implementations of __aenter__ and __aexit__ should be adequate for all subclasses.

abstractmethod await aclose()

Close this resource, possibly blocking.

IMPORTANT: This method may block in order to perform a “graceful” shutdown. But, if this fails, then it still must close any underlying resources before returning. An error from this method indicates a failure to achieve grace, not a failure to close the connection.

For example, suppose we call aclose() on a TLS-encrypted connection. This requires sending a “goodbye” message; but if the peer has become non-responsive, then our attempt to send this message might block forever, and eventually time out and be cancelled. In this case the aclose() method on SSLStream will immediately close the underlying transport stream using trio.aclose_forcefully() before raising Cancelled.

If the resource is already closed, then this method should silently succeed.

Once this method completes, any other pending or future operations on this resource should generally raise ClosedResourceError, unless there’s a good reason to do otherwise.

See also: trio.aclose_forcefully().

await trio.aclose_forcefully(resource)

Close an async resource or async generator immediately, without blocking to do any graceful cleanup.

AsyncResource objects guarantee that if their aclose() method is cancelled, then they will still close the resource (albeit in a potentially ungraceful fashion). aclose_forcefully() is a convenience function that exploits this behavior to let you force a resource to be closed without blocking: it works by calling await resource.aclose() and then cancelling it immediately.

Most users won’t need this, but it may be useful on cleanup paths where you can’t afford to block, or if you want to close a resource and don’t care about handling it gracefully. For example, if SSLStream encounters an error and cannot perform its own graceful close, then there’s no point in waiting to gracefully shut down the underlying transport either, so it calls await aclose_forcefully(self.transport_stream).

Note that this function is async, and that it acts as a checkpoint, but unlike most async functions it cannot block indefinitely (at least, assuming the underlying resource object is correctly implemented).

class trio.abc.SendStream

Bases: trio.abc.AsyncResource

A standard interface for sending data on a byte stream.

The underlying stream may be unidirectional, or bidirectional. If it’s bidirectional, then you probably want to also implement ReceiveStream, which makes your object a Stream.

SendStream objects also implement the AsyncResource interface, so they can be closed by calling aclose() or using an async with block.

If you want to send Python objects rather than raw bytes, see SendChannel.

abstractmethod await send_all(data)

Sends the given data through the stream, blocking if necessary.

Parameters

data (bytes, bytearray, or memoryview) – The data to send.

Raises

Most low-level operations in Trio provide a guarantee: if they raise trio.Cancelled, this means that they had no effect, so the system remains in a known state. This is not true for send_all(). If this operation raises trio.Cancelled (or any other exception for that matter), then it may have sent some, all, or none of the requested data, and there is no way to know which.

abstractmethod await wait_send_all_might_not_block()

Block until it’s possible that send_all() might not block.

This method may return early: it’s possible that after it returns, send_all() will still block. (In the worst case, if no better implementation is available, then it might always return immediately without blocking. It’s nice to do better than that when possible, though.)

This method must not return late: if it’s possible for send_all() to complete without blocking, then it must return. When implementing it, err on the side of returning early.

Raises

Note

This method is intended to aid in implementing protocols that want to delay choosing which data to send until the last moment. E.g., suppose you’re working on an implementation of a remote display server like VNC, and the network connection is currently backed up so that if you call send_all() now then it will sit for 0.5 seconds before actually sending anything. In this case it doesn’t make sense to take a screenshot, then wait 0.5 seconds, and then send it, because the screen will keep changing while you wait; it’s better to wait 0.5 seconds, then take the screenshot, and then send it, because this way the data you deliver will be more up-to-date. Using wait_send_all_might_not_block() makes it possible to implement the better strategy.

If you use this method, you might also want to read up on TCP_NOTSENT_LOWAT.

Further reading:

class trio.abc.ReceiveStream

Bases: trio.abc.AsyncResource

A standard interface for receiving data on a byte stream.

The underlying stream may be unidirectional, or bidirectional. If it’s bidirectional, then you probably want to also implement SendStream, which makes your object a Stream.

ReceiveStream objects also implement the AsyncResource interface, so they can be closed by calling aclose() or using an async with block.

If you want to receive Python objects rather than raw bytes, see ReceiveChannel.

ReceiveStream objects can be used in async for loops. Each iteration will produce an arbitrary sized chunk of bytes, like calling receive_some with no arguments. Every chunk will contain at least one byte, and the loop automatically exits when reaching end-of-file.

abstractmethod await receive_some(max_bytes=None)

Wait until there is data available on this stream, and then return some of it.

A return value of b"" (an empty bytestring) indicates that the stream has reached end-of-file. Implementations should be careful that they return b"" if, and only if, the stream has reached end-of-file!

Parameters

max_bytes (int) – The maximum number of bytes to return. Must be greater than zero. Optional; if omitted, then the stream object is free to pick a reasonable default.

Returns

The data received.

Return type

bytes or bytearray

Raises
class trio.abc.Stream

Bases: trio.abc.SendStream, trio.abc.ReceiveStream

A standard interface for interacting with bidirectional byte streams.

A Stream is an object that implements both the SendStream and ReceiveStream interfaces.

If implementing this interface, you should consider whether you can go one step further and implement HalfCloseableStream.

class trio.abc.HalfCloseableStream

Bases: trio.abc.Stream

This interface extends Stream to also allow closing the send part of the stream without closing the receive part.

abstractmethod await send_eof()

Send an end-of-file indication on this stream, if possible.

The difference between send_eof() and aclose() is that send_eof() is a unidirectional end-of-file indication. After you call this method, you shouldn’t try sending any more data on this stream, and your remote peer should receive an end-of-file indication (eventually, after receiving all the data you sent before that). But, they may continue to send data to you, and you can continue to receive it by calling receive_some(). You can think of it as calling aclose() on just the SendStream “half” of the stream object (and in fact that’s literally how trio.StapledStream implements it).

Examples:

  • On a socket, this corresponds to shutdown(..., SHUT_WR) (man page).

  • The SSH protocol provides the ability to multiplex bidirectional “channels” on top of a single encrypted connection. A Trio implementation of SSH could expose these channels as HalfCloseableStream objects, and calling send_eof() would send an SSH_MSG_CHANNEL_EOF request (see RFC 4254 §5.3).

  • On an SSL/TLS-encrypted connection, the protocol doesn’t provide any way to do a unidirectional shutdown without closing the connection entirely, so SSLStream implements Stream, not HalfCloseableStream.

If an EOF has already been sent, then this method should silently succeed.

Raises
class trio.abc.Listener(*args, **kwds)

Bases: trio.abc.AsyncResource, typing.Generic

A standard interface for listening for incoming connections.

Listener objects also implement the AsyncResource interface, so they can be closed by calling aclose() or using an async with block.

abstractmethod await accept()

Wait until an incoming connection arrives, and then return it.

Returns

An object representing the incoming connection. In practice this is generally some kind of Stream, but in principle you could also define a Listener that returned, say, channel objects.

Return type

AsyncResource

Raises

Listeners don’t generally raise BrokenResourceError, because for listeners there is no general condition of “the network/remote peer broke the connection” that can be handled in a generic way, like there is for streams. Other errors can occur and be raised from accept() – for example, if you run out of file descriptors then you might get an OSError with its errno set to EMFILE.

class trio.abc.SendChannel(*args, **kwds)

Bases: trio.abc.AsyncResource, typing.Generic

A standard interface for sending Python objects to some receiver.

SendChannel objects also implement the AsyncResource interface, so they can be closed by calling aclose or using an async with block.

If you want to send raw bytes rather than Python objects, see SendStream.

abstractmethod await send(value: SendType)None

Attempt to send an object through the channel, blocking if necessary.

Parameters

value (object) – The object to send.

Raises
class trio.abc.ReceiveChannel(*args, **kwds)

Bases: trio.abc.AsyncResource, typing.Generic

A standard interface for receiving Python objects from some sender.

You can iterate over a ReceiveChannel using an async for loop:

async for value in receive_channel:
    ...

This is equivalent to calling receive() repeatedly. The loop exits without error when receive raises EndOfChannel.

ReceiveChannel objects also implement the AsyncResource interface, so they can be closed by calling aclose or using an async with block.

If you want to receive raw bytes rather than Python objects, see ReceiveStream.

abstractmethod await receive() → ReceiveType

Attempt to receive an incoming object, blocking if necessary.

Returns

Whatever object was received.

Return type

object

Raises
class trio.abc.Channel(*args, **kwds)

Bases: trio.abc.SendChannel, trio.abc.ReceiveChannel

A standard interface for interacting with bidirectional channels.

A Channel is an object that implements both the SendChannel and ReceiveChannel interfaces, so you can both send and receive objects.

Generic stream tools

Trio currently provides a generic helper for writing servers that listen for connections using one or more Listeners, and a generic utility class for working with streams. And if you want to test code that’s written against the streams interface, you should also check out Streams in trio.testing.

await trio.serve_listeners(handler, listeners, *, handler_nursery=None, task_status=TASK_STATUS_IGNORED)

Listen for incoming connections on listeners, and for each one start a task running handler(stream).

Warning

If handler raises an exception, then this function doesn’t do anything special to catch it – so by default the exception will propagate out and crash your server. If you don’t want this, then catch exceptions inside your handler, or use a handler_nursery object that responds to exceptions in some other way.

Parameters
  • handler – An async callable, that will be invoked like handler_nursery.start_soon(handler, stream) for each incoming connection.

  • listeners – A list of Listener objects. serve_listeners() takes responsibility for closing them.

  • handler_nursery – The nursery used to start handlers, or any object with a start_soon method. If None (the default), then serve_listeners() will create a new nursery internally and use that.

  • task_status – This function can be used with nursery.start, which will return listeners.

Returns

This function never returns unless cancelled.

Resource handling:

If handler neglects to close the stream, then it will be closed using trio.aclose_forcefully().

Error handling:

Most errors coming from accept() are allowed to propagate out (crashing the server in the process). However, some errors – those which indicate that the server is temporarily overloaded – are handled specially. These are OSErrors with one of the following errnos:

  • EMFILE: process is out of file descriptors

  • ENFILE: system is out of file descriptors

  • ENOBUFS, ENOMEM: the kernel hit some sort of memory limitation when trying to create a socket object

When serve_listeners() gets one of these errors, then it:

  • Logs the error to the standard library logger trio.serve_listeners (level = ERROR, with exception information included). By default this causes it to be printed to stderr.

  • Waits 100 ms before calling accept again, in hopes that the system will recover.

class trio.StapledStream(send_stream, receive_stream)

Bases: trio.abc.HalfCloseableStream

This class staples together two unidirectional streams to make single bidirectional stream.

Parameters
  • send_stream (SendStream) – The stream to use for sending.

  • receive_stream (ReceiveStream) – The stream to use for receiving.

Example

A silly way to make a stream that echoes back whatever you write to it:

left, right = trio.testing.memory_stream_pair()
echo_stream = StapledStream(SocketStream(left), SocketStream(right))
await echo_stream.send_all(b"x")
assert await echo_stream.receive_some() == b"x"

StapledStream objects implement the methods in the HalfCloseableStream interface. They also have two additional public attributes:

send_stream

The underlying SendStream. send_all() and wait_send_all_might_not_block() are delegated to this object.

receive_stream

The underlying ReceiveStream. receive_some() is delegated to this object.

await aclose()

Calls aclose on both underlying streams.

await receive_some(max_bytes=None)

Calls self.receive_stream.receive_some.

await send_all(data)

Calls self.send_stream.send_all.

await send_eof()

Shuts down the send side of the stream.

If self.send_stream.send_eof exists, then calls it. Otherwise, calls self.send_stream.aclose().

await wait_send_all_might_not_block()

Calls self.send_stream.wait_send_all_might_not_block.

Sockets and networking

The high-level network interface is built on top of our stream abstraction.

await trio.open_tcp_stream(host, port, *, happy_eyeballs_delay=0.25, local_address=None)

Connect to the given host and port over TCP.

If the given host has multiple IP addresses associated with it, then we have a problem: which one do we use?

One approach would be to attempt to connect to the first one, and then if that fails, attempt to connect to the second one … until we’ve tried all of them. But the problem with this is that if the first IP address is unreachable (for example, because it’s an IPv6 address and our network discards IPv6 packets), then we might end up waiting tens of seconds for the first connection attempt to timeout before we try the second address.

Another approach would be to attempt to connect to all of the addresses at the same time, in parallel, and then use whichever connection succeeds first, abandoning the others. This would be fast, but create a lot of unnecessary load on the network and the remote server.

This function strikes a balance between these two extremes: it works its way through the available addresses one at a time, like the first approach; but, if happy_eyeballs_delay seconds have passed and it’s still waiting for an attempt to succeed or fail, then it gets impatient and starts the next connection attempt in parallel. As soon as any one connection attempt succeeds, all the other attempts are cancelled. This avoids unnecessary load because most connections will succeed after just one or two attempts, but if one of the addresses is unreachable then it doesn’t slow us down too much.

This is known as a “happy eyeballs” algorithm, and our particular variant is modelled after how Chrome connects to webservers; see RFC 6555 for more details.

Parameters
  • host (str or bytes) – The host to connect to. Can be an IPv4 address, IPv6 address, or a hostname.

  • port (int) – The port to connect to.

  • happy_eyeballs_delay (float) – How many seconds to wait for each connection attempt to succeed or fail before getting impatient and starting another one in parallel. Set to math.inf if you want to limit to only one connection attempt at a time (like socket.create_connection()). Default: 0.25 (250 ms).

  • local_address (None or str) –

    The local IP address or hostname to use as the source for outgoing connections. If None, we let the OS pick the source IP.

    This is useful in some exotic networking configurations where your host has multiple IP addresses, and you want to force the use of a specific one.

    Note that if you pass an IPv4 local_address, then you won’t be able to connect to IPv6 hosts, and vice-versa. If you want to take advantage of this to force the use of IPv4 or IPv6 without specifying an exact source address, you can use the IPv4 wildcard address local_address="0.0.0.0", or the IPv6 wildcard address local_address="::".

Returns

a Stream connected to the given server.

Return type

SocketStream

Raises

OSError – if the connection fails.

See also

open_ssl_over_tcp_stream

await trio.serve_tcp(handler, port, *, host=None, backlog=None, handler_nursery=None, task_status=TASK_STATUS_IGNORED)

Listen for incoming TCP connections, and for each one start a task running handler(stream).

This is a thin convenience wrapper around open_tcp_listeners() and serve_listeners() – see them for full details.

Warning

If handler raises an exception, then this function doesn’t do anything special to catch it – so by default the exception will propagate out and crash your server. If you don’t want this, then catch exceptions inside your handler, or use a handler_nursery object that responds to exceptions in some other way.

When used with nursery.start you get back the newly opened listeners. So, for example, if you want to start a server in your test suite and then connect to it to check that it’s working properly, you can use something like:

from trio.testing import open_stream_to_socket_listener

async with trio.open_nursery() as nursery:
    listeners = await nursery.start(serve_tcp, handler, 0)
    client_stream = await open_stream_to_socket_listener(listeners[0])

    # Then send and receive data on 'client_stream', for example:
    await client_stream.send_all(b"GET / HTTP/1.0\r\n\r\n")

This avoids several common pitfalls:

  1. It lets the kernel pick a random open port, so your test suite doesn’t depend on any particular port being open.

  2. It waits for the server to be accepting connections on that port before start returns, so there’s no race condition where the incoming connection arrives before the server is ready.

  3. It uses the Listener object to find out which port was picked, so it can connect to the right place.

Parameters
  • handler – The handler to start for each incoming connection. Passed to serve_listeners().

  • port – The port to listen on. Use 0 to let the kernel pick an open port. Passed to open_tcp_listeners().

  • host (str, bytes, or None) – The host interface to listen on; use None to bind to the wildcard address. Passed to open_tcp_listeners().

  • backlog – The listen backlog, or None to have a good default picked. Passed to open_tcp_listeners().

  • handler_nursery – The nursery to start handlers in, or None to use an internal nursery. Passed to serve_listeners().

  • task_status – This function can be used with nursery.start.

Returns

This function only returns when cancelled.

await trio.open_ssl_over_tcp_stream(host, port, *, https_compatible=False, ssl_context=None, happy_eyeballs_delay=0.25)

Make a TLS-encrypted Connection to the given host and port over TCP.

This is a convenience wrapper that calls open_tcp_stream() and wraps the result in an SSLStream.

This function does not perform the TLS handshake; you can do it manually by calling do_handshake(), or else it will be performed automatically the first time you send or receive data.

Parameters
  • host (bytes or str) – The host to connect to. We require the server to have a TLS certificate valid for this hostname.

  • port (int) – The port to connect to.

  • https_compatible (bool) – Set this to True if you’re connecting to a web server. See SSLStream for details. Default: False.

  • ssl_context (SSLContext or None) – The SSL context to use. If None (the default), ssl.create_default_context() will be called to create a context.

  • happy_eyeballs_delay (float) – See open_tcp_stream().

Returns

the encrypted connection to the server.

Return type

trio.SSLStream

await trio.serve_ssl_over_tcp(handler, port, ssl_context, *, host=None, https_compatible=False, backlog=None, handler_nursery=None, task_status=TASK_STATUS_IGNORED)

Listen for incoming TCP connections, and for each one start a task running handler(stream).

This is a thin convenience wrapper around open_ssl_over_tcp_listeners() and serve_listeners() – see them for full details.

Warning

If handler raises an exception, then this function doesn’t do anything special to catch it – so by default the exception will propagate out and crash your server. If you don’t want this, then catch exceptions inside your handler, or use a handler_nursery object that responds to exceptions in some other way.

When used with nursery.start you get back the newly opened listeners. See the documentation for serve_tcp() for an example where this is useful.

Parameters
  • handler – The handler to start for each incoming connection. Passed to serve_listeners().

  • port (int) – The port to listen on. Use 0 to let the kernel pick an open port. Ultimately passed to open_tcp_listeners().

  • ssl_context (SSLContext) – The SSL context to use for all incoming connections. Passed to open_ssl_over_tcp_listeners().

  • host (str, bytes, or None) – The address to bind to; use None to bind to the wildcard address. Ultimately passed to open_tcp_listeners().

  • https_compatible (bool) – Set this to True if you want to use “HTTPS-style” TLS. See SSLStream for details.

  • backlog (int or None) – See SSLStream for details.

  • handler_nursery – The nursery to start handlers in, or None to use an internal nursery. Passed to serve_listeners().

  • task_status – This function can be used with nursery.start.

Returns

This function only returns when cancelled.

await trio.open_unix_socket(filename)

Opens a connection to the specified Unix domain socket.

You must have read/write permission on the specified file to connect.

Parameters

filename (str or bytes) – The filename to open the connection to.

Returns

a Stream connected to the given file.

Return type

SocketStream

Raises
  • OSError – If the socket file could not be connected to.

  • RuntimeError – If AF_UNIX sockets are not supported.

class trio.SocketStream(socket)

Bases: trio.abc.HalfCloseableStream

An implementation of the trio.abc.HalfCloseableStream interface based on a raw network socket.

Parameters

socket – The Trio socket object to wrap. Must have type SOCK_STREAM, and be connected.

By default for TCP sockets, SocketStream enables TCP_NODELAY, and (on platforms where it’s supported) enables TCP_NOTSENT_LOWAT with a reasonable buffer size (currently 16 KiB) – see issue #72 for discussion. You can of course override these defaults by calling setsockopt().

Once a SocketStream object is constructed, it implements the full trio.abc.HalfCloseableStream interface. In addition, it provides a few extra features:

socket

The Trio socket object that this stream wraps.

await aclose()
getsockopt(level, option, buffersize=0)

Check the current value of an option on the underlying socket.

See socket.socket.getsockopt() for details.

await receive_some(max_bytes=None)
await send_all(data)
await send_eof()
setsockopt(level, option, value)

Set an option on the underlying socket.

See socket.socket.setsockopt() for details.

await wait_send_all_might_not_block()
class trio.SocketListener(socket)

Bases: trio.abc.Listener

A Listener that uses a listening socket to accept incoming connections as SocketStream objects.

Parameters

socket – The Trio socket object to wrap. Must have type SOCK_STREAM, and be listening.

Note that the SocketListener “takes ownership” of the given socket; closing the SocketListener will also close the socket.

socket

The Trio socket object that this stream wraps.

await accept()

Accept an incoming connection.

Returns

SocketStream

Raises
  • OSError – if the underlying call to accept raises an unexpected error.

  • ClosedResourceError – if you already closed the socket.

This method handles routine errors like ECONNABORTED, but passes other errors on to its caller. In particular, it does not make any special effort to handle resource exhaustion errors like EMFILE, ENFILE, ENOBUFS, ENOMEM.

await aclose()

Close this listener and its underlying socket.

await trio.open_tcp_listeners(port, *, host=None, backlog=None)

Create SocketListener objects to listen for TCP connections.

Parameters
  • port (int) –

    The port to listen on.

    If you use 0 as your port, then the kernel will automatically pick an arbitrary open port. But be careful: if you use this feature when binding to multiple IP addresses, then each IP address will get its own random port, and the returned listeners will probably be listening on different ports. In particular, this will happen if you use host=None – which is the default – because in this case open_tcp_listeners() will bind to both the IPv4 wildcard address (0.0.0.0) and also the IPv6 wildcard address (::).

  • host (str, bytes-like, or None) –

    The local interface to bind to. This is passed to getaddrinfo() with the AI_PASSIVE flag set.

    If you want to bind to the wildcard address on both IPv4 and IPv6, in order to accept connections on all available interfaces, then pass None. This is the default.

    If you have a specific interface you want to bind to, pass its IP address or hostname here. If a hostname resolves to multiple IP addresses, this function will open one listener on each of them.

    If you want to use only IPv4, or only IPv6, but want to accept on all interfaces, pass the family-specific wildcard address: "0.0.0.0" for IPv4-only and "::" for IPv6-only.

  • backlog (int or None) – The listen backlog to use. If you leave this as None then Trio will pick a good default. (Currently: whatever your system has configured as the maximum backlog.)

Returns

list of SocketListener

await trio.open_ssl_over_tcp_listeners(port, ssl_context, *, host=None, https_compatible=False, backlog=None)

Start listening for SSL/TLS-encrypted TCP connections to the given port.

Parameters

SSL / TLS support

Trio provides SSL/TLS support based on the standard library ssl module. Trio’s SSLStream and SSLListener take their configuration from a ssl.SSLContext, which you can create using ssl.create_default_context() and customize using the other constants and functions in the ssl module.

Warning

Avoid instantiating ssl.SSLContext directly. A newly constructed SSLContext has less secure defaults than one returned by ssl.create_default_context().

Instead of using ssl.SSLContext.wrap_socket(), you create a SSLStream:

class trio.SSLStream(transport_stream, ssl_context, *, server_hostname=None, server_side=False, https_compatible=False)

Bases: trio.abc.Stream

Encrypted communication using SSL/TLS.

SSLStream wraps an arbitrary Stream, and allows you to perform encrypted communication over it using the usual Stream interface. You pass regular data to send_all(), then it encrypts it and sends the encrypted data on the underlying Stream; receive_some() takes encrypted data out of the underlying Stream and decrypts it before returning it.

You should read the standard library’s ssl documentation carefully before attempting to use this class, and probably other general documentation on SSL/TLS as well. SSL/TLS is subtle and quick to anger. Really. I’m not kidding.

Parameters
  • transport_stream (Stream) – The stream used to transport encrypted data. Required.

  • ssl_context (SSLContext) – The SSLContext used for this connection. Required. Usually created by calling ssl.create_default_context().

  • server_hostname (str or None) – The name of the server being connected to. Used for SNI and for validating the server’s certificate (if hostname checking is enabled). This is effectively mandatory for clients, and actually mandatory if ssl_context.check_hostname is True.

  • server_side (bool) – Whether this stream is acting as a client or server. Defaults to False, i.e. client mode.

  • https_compatible (bool) –

    There are two versions of SSL/TLS commonly encountered in the wild: the standard version, and the version used for HTTPS (HTTP-over-SSL/TLS).

    Standard-compliant SSL/TLS implementations always send a cryptographically signed close_notify message before closing the connection. This is important because if the underlying transport were simply closed, then there wouldn’t be any way for the other side to know whether the connection was intentionally closed by the peer that they negotiated a cryptographic connection to, or by some man-in-the-middle attacker who can’t manipulate the cryptographic stream, but can manipulate the transport layer (a so-called “truncation attack”).

    However, this part of the standard is widely ignored by real-world HTTPS implementations, which means that if you want to interoperate with them, then you NEED to ignore it too.

    Fortunately this isn’t as bad as it sounds, because the HTTP protocol already includes its own equivalent of close_notify, so doing this again at the SSL/TLS level is redundant. But not all protocols do! Therefore, by default Trio implements the safer standard-compliant version (https_compatible=False). But if you’re speaking HTTPS or some other protocol where close_notifys are commonly skipped, then you should set https_compatible=True; with this setting, Trio will neither expect nor send close_notify messages.

    If you have code that was written to use ssl.SSLSocket and now you’re porting it to Trio, then it may be useful to know that a difference between SSLStream and ssl.SSLSocket is that SSLSocket implements the https_compatible=True behavior by default.

transport_stream

The underlying transport stream that was passed to __init__. An example of when this would be useful is if you’re using SSLStream over a SocketStream and want to call the SocketStream’s setsockopt() method.

Type

trio.abc.Stream

Internally, this class is implemented using an instance of ssl.SSLObject, and all of SSLObject’s methods and attributes are re-exported as methods and attributes on this class. However, there is one difference: SSLObject has several methods that return information about the encrypted connection, like cipher() or selected_alpn_protocol(). If you call them before the handshake, when they can’t possibly return useful data, then ssl.SSLObject returns None, but trio.SSLStream raises NeedHandshakeError.

This also means that if you register a SNI callback using sni_callback, then the first argument your callback receives will be a ssl.SSLObject.

await aclose()

Gracefully shut down this connection, and close the underlying transport.

If https_compatible is False (the default), then this attempts to first send a close_notify and then close the underlying stream by calling its aclose() method.

If https_compatible is set to True, then this simply closes the underlying stream and marks this stream as closed.

await do_handshake()

Ensure that the initial handshake has completed.

The SSL protocol requires an initial handshake to exchange certificates, select cryptographic keys, and so forth, before any actual data can be sent or received. You don’t have to call this method; if you don’t, then SSLStream will automatically perform the handshake as needed, the first time you try to send or receive data. But if you want to trigger it manually – for example, because you want to look at the peer’s certificate before you start talking to them – then you can call this method.

If the initial handshake is already in progress in another task, this waits for it to complete and then returns.

If the initial handshake has already completed, this returns immediately without doing anything (except executing a checkpoint).

Warning

If this method is cancelled, then it may leave the SSLStream in an unusable state. If this happens then any future attempt to use the object will raise trio.BrokenResourceError.

await receive_some(max_bytes=None)

Read some data from the underlying transport, decrypt it, and return it.

See trio.abc.ReceiveStream.receive_some() for details.

Warning

If this method is cancelled while the initial handshake or a renegotiation are in progress, then it may leave the SSLStream in an unusable state. If this happens then any future attempt to use the object will raise trio.BrokenResourceError.

await send_all(data)

Encrypt some data and then send it on the underlying transport.

See trio.abc.SendStream.send_all() for details.

Warning

If this method is cancelled, then it may leave the SSLStream in an unusable state. If this happens then any attempt to use the object will raise trio.BrokenResourceError.

await unwrap()

Cleanly close down the SSL/TLS encryption layer, allowing the underlying stream to be used for unencrypted communication.

You almost certainly don’t need this.

Returns

A pair (transport_stream, trailing_bytes), where transport_stream is the underlying transport stream, and trailing_bytes is a byte string. Since SSLStream doesn’t necessarily know where the end of the encrypted data will be, it can happen that it accidentally reads too much from the underlying stream. trailing_bytes contains this extra data; you should process it as if it was returned from a call to transport_stream.receive_some(...).

await wait_send_all_might_not_block()

See trio.abc.SendStream.wait_send_all_might_not_block().

And if you’re implementing a server, you can use SSLListener:

class trio.SSLListener(transport_listener, ssl_context, *, https_compatible=False)

Bases: trio.abc.Listener

A Listener for SSL/TLS-encrypted servers.

SSLListener wraps around another Listener, and converts all incoming connections to encrypted connections by wrapping them in a SSLStream.

Parameters
transport_listener

The underlying listener that was passed to __init__.

Type

trio.abc.Listener

await accept()

Accept the next connection and wrap it in an SSLStream.

See trio.abc.Listener.accept() for details.

await aclose()

Close the transport listener.

Some methods on SSLStream raise NeedHandshakeError if you call them before the handshake completes:

exception trio.NeedHandshakeError

Some SSLStream methods can’t return any meaningful data until after the handshake. If you call them before the handshake, they raise this error.

Low-level networking with trio.socket

The trio.socket module provides Trio’s basic low-level networking API. If you’re doing ordinary things with stream-oriented connections over IPv4/IPv6/Unix domain sockets, then you probably want to stick to the high-level API described above. If you want to use UDP, or exotic address families like AF_BLUETOOTH, or otherwise get direct access to all the quirky bits of your system’s networking API, then you’re in the right place.

Top-level exports

Generally, the API exposed by trio.socket mirrors that of the standard library socket module. Most constants (like SOL_SOCKET) and simple utilities (like inet_aton()) are simply re-exported unchanged. But there are also some differences, which are described here.

First, Trio provides analogues to all the standard library functions that return socket objects; their interface is identical, except that they’re modified to return Trio socket objects instead:

trio.socket.socket(family=- 1, type=- 1, proto=- 1, fileno=None)

Create a new Trio socket, like socket.socket.

This function’s behavior can be customized using set_custom_socket_factory().

trio.socket.socketpair(family=None, type=<SocketKind.SOCK_STREAM: 1>, proto=0)

Like socket.socketpair(), but returns a pair of Trio socket objects.

trio.socket.fromfd(fd, family, type, proto=0)

Like socket.fromfd(), but returns a Trio socket object.

trio.socket.fromshare(data)

Like socket.fromshare(), but returns a Trio socket object.

In addition, there is a new function to directly convert a standard library socket into a Trio socket:

trio.socket.from_stdlib_socket(sock)

Convert a standard library socket.socket object into a Trio socket object.

Unlike socket.socket, trio.socket.socket() is a function, not a class; if you want to check whether an object is a Trio socket, use isinstance(obj, trio.socket.SocketType).

For name lookup, Trio provides the standard functions, but with some changes:

await trio.socket.getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)

Look up a numeric address given a name.

Arguments and return values are identical to socket.getaddrinfo(), except that this version is async.

Also, trio.socket.getaddrinfo() correctly uses IDNA 2008 to process non-ASCII domain names. (socket.getaddrinfo() uses IDNA 2003, which can give the wrong result in some cases and cause you to connect to a different host than the one you intended; see bpo-17305.)

This function’s behavior can be customized using set_custom_hostname_resolver().

await trio.socket.getnameinfo(sockaddr, flags)

Look up a name given a numeric address.

Arguments and return values are identical to socket.getnameinfo(), except that this version is async.

This function’s behavior can be customized using set_custom_hostname_resolver().

await trio.socket.getprotobyname(name)

Look up a protocol number by name. (Rarely used.)

Like socket.getprotobyname(), but async.

Trio intentionally DOES NOT include some obsolete, redundant, or broken features:

Socket objects

class trio.socket.SocketType

Note

trio.socket.SocketType is an abstract class and cannot be instantiated directly; you get concrete socket objects by calling constructors like trio.socket.socket(). However, you can use it to check if an object is a Trio socket via isinstance(obj, trio.socket.SocketType).

Trio socket objects are overall very similar to the standard library socket objects, with a few important differences:

First, and most obviously, everything is made “Trio-style”: blocking methods become async methods, and the following attributes are not supported:

  • setblocking(): Trio sockets always act like blocking sockets; if you need to read/write from multiple sockets at once, then create multiple tasks.

  • settimeout(): see Cancellation and timeouts instead.

  • makefile(): Python’s file-like API is synchronous, so it can’t be implemented on top of an async socket.

  • sendall(): Could be supported, but you’re better off using the higher-level SocketStream, and specifically its send_all() method, which also does additional error checking.

In addition, the following methods are similar to the equivalents in socket.socket, but have some Trio-specific quirks:

await connect()

Connect the socket to a remote address.

Similar to socket.socket.connect(), except async.

Warning

Due to limitations of the underlying operating system APIs, it is not always possible to properly cancel a connection attempt once it has begun. If connect() is cancelled, and is unable to abort the connection attempt, then it will:

  1. forcibly close the socket to prevent accidental re-use

  2. raise Cancelled.

tl;dr: if connect() is cancelled then the socket is left in an unknown state – possibly open, and possibly closed. The only reasonable thing to do is to close it.

is_readable()

Check whether the socket is readable or not.

sendfile()

Not implemented yet!

We also keep track of an extra bit of state, because it turns out to be useful for trio.SocketStream:

did_shutdown_SHUT_WR

This bool attribute is True if you’ve called sock.shutdown(SHUT_WR) or sock.shutdown(SHUT_RDWR), and False otherwise.

The following methods are identical to their equivalents in socket.socket, except async, and the ones that take address arguments require pre-resolved addresses:

All methods and attributes not mentioned above are identical to their equivalents in socket.socket:

Asynchronous filesystem I/O

Trio provides built-in facilities for performing asynchronous filesystem operations like reading or renaming a file. Generally, we recommend that you use these instead of Python’s normal synchronous file APIs. But the tradeoffs here are somewhat subtle: sometimes people switch to async I/O, and then they’re surprised and confused when they find it doesn’t speed up their program. The next section explains the theory behind async file I/O, to help you better understand your code’s behavior. Or, if you just want to get started, you can jump down to the API overview.

Background: Why is async file I/O useful? The answer may surprise you

Many people expect that switching from synchronous file I/O to async file I/O will always make their program faster. This is not true! If we just look at total throughput, then async file I/O might be faster, slower, or about the same, and it depends in a complicated way on things like your exact patterns of disk access, or how much RAM you have. The main motivation for async file I/O is not to improve throughput, but to reduce the frequency of latency glitches.

To understand why, you need to know two things.

First, right now no mainstream operating system offers a generic, reliable, native API for async file or filesystem operations, so we have to fake it by using threads (specifically, trio.to_thread.run_sync()). This is cheap but isn’t free: on a typical PC, dispatching to a worker thread adds something like ~100 µs of overhead to each operation. (“µs” is pronounced “microseconds”, and there are 1,000,000 µs in a second. Note that all the numbers here are going to be rough orders of magnitude to give you a sense of scale; if you need precise numbers for your environment, measure!)

And second, the cost of a disk operation is incredibly bimodal. Sometimes, the data you need is already cached in RAM, and then accessing it is very, very fast – calling io.FileIO's read method on a cached file takes on the order of ~1 µs. But when the data isn’t cached, then accessing it is much, much slower: the average is ~100 µs for SSDs and ~10,000 µs for spinning disks, and if you look at tail latencies then for both types of storage you’ll see cases where occasionally some operation will be 10x or 100x slower than average. And that’s assuming your program is the only thing trying to use that disk – if you’re on some oversold cloud VM fighting for I/O with other tenants then who knows what will happen. And some operations can require multiple disk accesses.

Putting these together: if your data is in RAM then it should be clear that using a thread is a terrible idea – if you add 100 µs of overhead to a 1 µs operation, then that’s a 100x slowdown! On the other hand, if your data’s on a spinning disk, then using a thread is great – instead of blocking the main thread and all tasks for 10,000 µs, we only block them for 100 µs and can spend the rest of that time running other tasks to get useful work done, which can effectively be a 100x speedup.

But here’s the problem: for any individual I/O operation, there’s no way to know in advance whether it’s going to be one of the fast ones or one of the slow ones, so you can’t pick and choose. When you switch to async file I/O, it makes all the fast operations slower, and all the slow operations faster. Is that a win? In terms of overall speed, it’s hard to say: it depends what kind of disks you’re using and your kernel’s disk cache hit rate, which in turn depends on your file access patterns, how much spare RAM you have, the load on your service, … all kinds of things. If the answer is important to you, then there’s no substitute for measuring your code’s actual behavior in your actual deployment environment. But what we can say is that async disk I/O makes performance much more predictable across a wider range of runtime conditions.

If you’re not sure what to do, then we recommend that you use async disk I/O by default, because it makes your code more robust when conditions are bad, especially with regards to tail latencies; this improves the chances that what your users see matches what you saw in testing. Blocking the main thread stops all tasks from running for that time. 10,000 µs is 10 ms, and it doesn’t take many 10 ms glitches to start adding up to real money; async disk I/O can help prevent those. Just don’t expect it to be magic, and be aware of the tradeoffs.

API overview

If you want to perform general filesystem operations like creating and listing directories, renaming files, or checking file metadata – or if you just want a friendly way to work with filesystem paths – then you want trio.Path. It’s an asyncified replacement for the standard library’s pathlib.Path, and provides the same comprehensive set of operations.

For reading and writing to files and file-like objects, Trio also provides a mechanism for wrapping any synchronous file-like object into an asynchronous interface. If you have a trio.Path object you can get one of these by calling its open() method; or if you know the file’s name you can open it directly with trio.open_file(). Alternatively, if you already have an open file-like object, you can wrap it with trio.wrap_file() – one case where this is especially useful is to wrap io.BytesIO or io.StringIO when writing tests.

Asynchronous path objects

class trio.Path(*args)

A pathlib.Path wrapper that executes blocking methods in trio.to_thread.run_sync().

as_posix()

Return the string representation of the path with forward (/) slashes.

as_uri()

Return the path as a ‘file’ URI.

await chmod(*args, **kwargs)

Like chmod(), but async.

classmethod await cwd(*args, **kwargs)

Like cwd(), but async.

await exists(*args, **kwargs)

Like exists(), but async.

await expanduser(*args, **kwargs)

Like expanduser(), but async.

await glob(*args, **kwargs)

Like glob(), but async.

await group(*args, **kwargs)

Like group(), but async.

classmethod await home(*args, **kwargs)

Like home(), but async.

is_absolute()

True if the path is absolute (has both a root and, if applicable, a drive).

await is_block_device(*args, **kwargs)

Like is_block_device(), but async.

await is_char_device(*args, **kwargs)

Like is_char_device(), but async.

await is_dir(*args, **kwargs)

Like is_dir(), but async.

await is_fifo(*args, **kwargs)

Like is_fifo(), but async.

await is_file(*args, **kwargs)

Like is_file(), but async.

await is_mount(*args, **kwargs)

Like is_mount(), but async.

is_reserved()

Return True if the path contains one of the special names reserved by the system, if any.

await is_socket(*args, **kwargs)

Like is_socket(), but async.

Like is_symlink(), but async.

await iterdir(*args, **kwargs)

Like pathlib.Path.iterdir(), but async.

This is an async method that returns a synchronous iterator, so you use it like:

for subpath in await mypath.iterdir():
    ...

Note that it actually loads the whole directory list into memory immediately, during the initial call. (See issue #501 for discussion.)

joinpath(*args)

Combine this path with one or several arguments, and return a new path representing either a subpath (if all arguments are relative paths) or a totally different path (if one of the arguments is anchored).

await lchmod(*args, **kwargs)

Like lchmod(), but async.

await lstat(*args, **kwargs)

Like lstat(), but async.

match(path_pattern)

Return True if this path matches the given pattern.

await mkdir(*args, **kwargs)

Like mkdir(), but async.

await open(mode='r', buffering=- 1, encoding=None, errors=None, newline=None)

Open the file pointed by this path and return a file object, as the built-in open() function does.

await owner(*args, **kwargs)

Like owner(), but async.

await read_bytes(*args, **kwargs)

Like read_bytes(), but async.

await read_text(*args, **kwargs)

Like read_text(), but async.

relative_to(*other)

Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not a subpath of the other path), raise ValueError.

await rename(*args, **kwargs)

Like rename(), but async.

await replace(*args, **kwargs)

Like replace(), but async.

await resolve(*args, **kwargs)

Like resolve(), but async.

await rglob(*args, **kwargs)

Like rglob(), but async.

await rmdir(*args, **kwargs)

Like rmdir(), but async.

await samefile(*args, **kwargs)

Like samefile(), but async.

await stat(*args, **kwargs)

Like stat(), but async.

Like symlink_to(), but async.

await touch(*args, **kwargs)

Like touch(), but async.

Like unlink(), but async.

with_name(name)

Return a new path with the file name changed.

with_suffix(suffix)

Return a new path with the file suffix changed. If the path has no suffix, add given suffix. If the given suffix is an empty string, remove the suffix from the path.

await write_bytes(*args, **kwargs)

Like write_bytes(), but async.

await write_text(*args, **kwargs)

Like write_text(), but async.

Asynchronous file objects

await trio.open_file(file, mode='r', buffering=- 1, encoding=None, errors=None, newline=None, closefd=True, opener=None)

Asynchronous version of io.open().

Returns

An asynchronous file object

Example:

async with await trio.open_file(filename) as f:
    async for line in f:
        pass

assert f.closed

See also

trio.Path.open()

trio.wrap_file(file)

This wraps any file object in a wrapper that provides an asynchronous file object interface.

Parameters

file – a file object

Returns

An asynchronous file object that wraps file

Example:

async_file = trio.wrap_file(StringIO('asdf'))

assert await async_file.read() == 'asdf'
Asynchronous file interface

Trio’s asynchronous file objects have an interface that automatically adapts to the object being wrapped. Intuitively, you can mostly treat them like a regular file object, except adding an await in front of any of methods that do I/O. The definition of file object is a little vague in Python though, so here are the details:

  • Synchronous attributes/methods: if any of the following attributes or methods are present, then they’re re-exported unchanged: closed, encoding, errors, fileno, isatty, newlines, readable, seekable, writable, buffer, raw, line_buffering, closefd, name, mode, getvalue, getbuffer.

  • Async methods: if any of the following methods are present, then they’re re-exported as an async method: flush, read, read1, readall, readinto, readline, readlines, seek, tell, truncate, write, writelines, readinto1, peek, detach.

Special notes:

  • Async file objects implement Trio’s AsyncResource interface: you close them by calling aclose() instead of close (!!), and they can be used as async context managers. Like all aclose() methods, the aclose method on async file objects is guaranteed to close the file before returning, even if it is cancelled or otherwise raises an error.

  • Using the same async file object from multiple tasks simultaneously: because the async methods on async file objects are implemented using threads, it’s only safe to call two of them at the same time from different tasks IF the underlying synchronous file object is thread-safe. You should consult the documentation for the object you’re wrapping. For objects returned from trio.open_file() or trio.Path.open(), it depends on whether you open the file in binary mode or text mode: binary mode files are task-safe/thread-safe, text mode files are not.

  • Async file objects can be used as async iterators to iterate over the lines of the file:

    async with await trio.open_file(...) as f:
        async for line in f:
            print(line)
    
  • The detach method, if present, returns an async file object.

This should include all the attributes exposed by classes in io. But if you’re wrapping an object that has other attributes that aren’t on the list above, then you can access them via the .wrapped attribute:

wrapped

The underlying synchronous file object.

Spawning subprocesses

Trio provides support for spawning other programs as subprocesses, communicating with them via pipes, sending them signals, and waiting for them to exit.

Most of the time, this is done through our high-level interface, trio.run_process. It lets you either run a process to completion while optionally capturing the output, or else run it in a background task and interact with it while it’s running:

await trio.run_process(command, *, stdin=b'', capture_stdout=False, capture_stderr=False, check=True, deliver_cancel=None, task_status=TASK_STATUS_IGNORED, **options)

Run command in a subprocess and wait for it to complete.

This function can be called in two different ways.

One option is a direct call, like:

completed_process_info = await trio.run_process(...)

In this case, it returns a subprocess.CompletedProcess instance describing the results. Use this if you want to treat a process like a function call.

The other option is to run it as a task using Nursery.start – the enhanced version of start_soon that lets a task pass back a value during startup:

process = await nursery.start(trio.run_process, ...)

In this case, start returns a Process object that you can use to interact with the process while it’s running. Use this if you want to treat a process like a background task.

Either way, run_process makes sure that the process has exited before returning, handles cancellation, optionally checks for errors, and provides some convenient shorthands for dealing with the child’s input/output.

Input: run_process supports all the same stdin= arguments as subprocess.Popen. In addition, if you simply want to pass in some fixed data, you can pass a plain bytes object, and run_process will take care of setting up a pipe, feeding in the data you gave, and then sending end-of-file. The default is b"", which means that the child will receive an empty stdin. If you want the child to instead read from the parent’s stdin, use stdin=None.

Output: By default, any output produced by the subprocess is passed through to the standard output and error streams of the parent Trio process.

When calling run_process directly, you can capture the subprocess’s output by passing capture_stdout=True to capture the subprocess’s standard output, and/or capture_stderr=True to capture its standard error. Captured data is collected up by Trio into an in-memory buffer, and then provided as the stdout and/or stderr attributes of the returned CompletedProcess object. The value for any stream that was not captured will be None.

If you want to capture both stdout and stderr while keeping them separate, pass capture_stdout=True, capture_stderr=True.

If you want to capture both stdout and stderr but mixed together in the order they were printed, use: capture_stdout=True, stderr=subprocess.STDOUT. This directs the child’s stderr into its stdout, so the combined output will be available in the stdout attribute.

If you’re using await nursery.start(trio.run_process, ...) and want to capture the subprocess’s output for further processing, then use stdout=subprocess.PIPE and then make sure to read the data out of the Process.stdout stream. If you want to capture stderr separately, use stderr=subprocess.PIPE. If you want to capture both, but mixed together in the correct order, use stdout=subproces.PIPE, stderr=subprocess.STDOUT.

Error checking: If the subprocess exits with a nonzero status code, indicating failure, run_process() raises a subprocess.CalledProcessError exception rather than returning normally. The captured outputs are still available as the stdout and stderr attributes of that exception. To disable this behavior, so that run_process() returns normally even if the subprocess exits abnormally, pass check=False.

Note that this can make the capture_stdout and capture_stderr arguments useful even when starting run_process as a task: if you only care about the output if the process fails, then you can enable capturing and then read the output off of the CalledProcessError.

Cancellation: If cancelled, run_process sends a termination request to the subprocess, then waits for it to fully exit. The deliver_cancel argument lets you control how the process is terminated.

Note

run_process is intentionally similar to the standard library subprocess.run, but some of the defaults are different. Specifically, we default to:

To get the subprocess.run semantics, use check=False, stdin=None.

Parameters
  • command (list or str) – The command to run. Typically this is a sequence of strings such as ['ls', '-l', 'directory with spaces'], where the first element names the executable to invoke and the other elements specify its arguments. With shell=True in the **options, or on Windows, command may alternatively be a string, which will be parsed following platform-dependent quoting rules.

  • stdin (bytes, subprocess.PIPE, file descriptor, or None) –

    The bytes to provide to the subprocess on its standard input stream, or None if the subprocess’s standard input should come from the same place as the parent Trio process’s standard input. As is the case with the subprocess module, you can also pass a file descriptor or an object with a fileno() method, in which case the subprocess’s standard input will come from that file.

    When starting run_process as a background task, you can also use stdin=subprocess.PIPE, in which case Process.stdin will be a SendStream that you can use to send data to the child.

  • capture_stdout (bool) – If true, capture the bytes that the subprocess writes to its standard output stream and return them in the stdout attribute of the returned subprocess.CompletedProcess or subprocess.CalledProcessError.

  • capture_stderr (bool) – If true, capture the bytes that the subprocess writes to its standard error stream and return them in the stderr attribute of the returned CompletedProcess or subprocess.CalledProcessError.

  • check (bool) – If false, don’t validate that the subprocess exits successfully. You should be sure to check the returncode attribute of the returned object if you pass check=False, so that errors don’t pass silently.

  • deliver_cancel (async function or None) –

    If run_process is cancelled, then it needs to kill the child process. There are multiple ways to do this, so we let you customize it.

    If you pass None (the default), then the behavior depends on the platform:

    • On Windows, Trio calls TerminateProcess, which should kill the process immediately.

    • On Unix-likes, the default behavior is to send a SIGTERM, wait 5 seconds, and send a SIGKILL.

    Alternatively, you can customize this behavior by passing in an arbitrary async function, which will be called with the Process object as an argument. For example, the default Unix behavior could be implemented like this:

    async def my_deliver_cancel(process):
        process.send_signal(signal.SIGTERM)
        await trio.sleep(5)
        process.send_signal(signal.SIGKILL)
    

    When the process actually exits, the deliver_cancel function will automatically be cancelled – so if the process exits after SIGTERM, then we’ll never reach the SIGKILL.

    In any case, run_process will always wait for the child process to exit before raising Cancelled.

  • **optionsrun_process() also accepts any general subprocess options and passes them on to the Process constructor. This includes the stdout and stderr options, which provide additional redirection possibilities such as stderr=subprocess.STDOUT, stdout=subprocess.DEVNULL, or file descriptors.

Returns

When called normally – a subprocess.CompletedProcess instance describing the return code and outputs.

When called via Nursery.start – a trio.Process instance.

Raises
  • UnicodeError – if stdin is specified as a Unicode string, rather than bytes

  • ValueError – if multiple redirections are specified for the same stream, e.g., both capture_stdout=True and stdout=subprocess.DEVNULL

  • subprocess.CalledProcessError – if check=False is not passed and the process exits with a nonzero exit status

  • OSError – if an error is encountered starting or communicating with the process

Note

The child process runs in the same process group as the parent Trio process, so a Ctrl+C will be delivered simultaneously to both parent and child. If you don’t want this behavior, consult your platform’s documentation for starting child processes in a different process group.

class trio.Process(*args, **kwargs)

A child process. Like subprocess.Popen, but async.

This class has no public constructor. The most common way to get a Process object is to combine Nursery.start with run_process:

process_object = await nursery.start(run_process, ...)

This way, run_process supervises the process and makes sure that it is cleaned up properly, while optionally checking the return value, feeding it input, and so on.

If you need more control – for example, because you want to spawn a child process that outlives your program – then another option is to use trio.lowlevel.open_process:

process_object = await trio.lowlevel.open_process(...)
args

The command passed at construction time, specifying the process to execute and its arguments.

Type

str or list

pid

The process ID of the child process managed by this object.

Type

int

stdin

A stream connected to the child’s standard input stream: when you write bytes here, they become available for the child to read. Only available if the Process was constructed using stdin=PIPE; otherwise this will be None.

Type

trio.abc.SendStream or None

stdout

A stream connected to the child’s standard output stream: when the child writes to standard output, the written bytes become available for you to read here. Only available if the Process was constructed using stdout=PIPE; otherwise this will be None.

Type

trio.abc.ReceiveStream or None

stderr

A stream connected to the child’s standard error stream: when the child writes to standard error, the written bytes become available for you to read here. Only available if the Process was constructed using stderr=PIPE; otherwise this will be None.

Type

trio.abc.ReceiveStream or None

stdio

A stream that sends data to the child’s standard input and receives from the child’s standard output. Only available if both stdin and stdout are available; otherwise this will be None.

Type

trio.StapledStream or None

returncode

The exit status of the process (an integer), or None if it’s still running.

By convention, a return code of zero indicates success. On UNIX, negative values indicate termination due to a signal, e.g., -11 if terminated by signal 11 (SIGSEGV). On Windows, a process that exits due to a call to Process.terminate() will have an exit status of 1.

Unlike the standard library subprocess.Popen.returncode, you don’t have to call poll or wait to update this attribute; it’s automatically updated as needed, and will always give you the latest information.

await wait()

Block until the process exits.

Returns

The exit status of the process; see returncode.

poll()

Returns the exit status of the process (an integer), or None if it’s still running.

Note that on Trio (unlike the standard library subprocess.Popen), process.poll() and process.returncode always give the same result. See returncode for more details. This method is only included to make it easier to port code from subprocess.

kill()

Immediately terminate the process.

On UNIX, this is equivalent to send_signal(signal.SIGKILL). On Windows, it calls TerminateProcess. In both cases, the process cannot prevent itself from being killed, but the termination will be delivered asynchronously; use wait() if you want to ensure the process is actually dead before proceeding.

terminate()

Terminate the process, politely if possible.

On UNIX, this is equivalent to send_signal(signal.SIGTERM); by convention this requests graceful termination, but a misbehaving or buggy process might ignore it. On Windows, terminate() forcibly terminates the process in the same manner as kill().

send_signal(sig)

Send signal sig to the process.

On UNIX, sig may be any signal defined in the signal module, such as signal.SIGINT or signal.SIGTERM. On Windows, it may be anything accepted by the standard library subprocess.Popen.send_signal().

Note

communicate() is not provided as a method on Process objects; call run_process() normally for simple capturing, or write the loop yourself if you have unusual needs. communicate() has quite unusual cancellation behavior in the standard library (on some platforms it spawns a background thread which continues to read from the child process even after the timeout has expired) and we wanted to provide an interface with fewer surprises.

If trio.run_process is too limiting, we also offer a low-level API, trio.lowlevel.open_process. For example, if you want to spawn a child process that will outlive the parent process and be orphaned, then run_process can’t do that, but open_process can.

Options for starting subprocesses

All of Trio’s subprocess APIs accept the numerous keyword arguments used by the standard subprocess module to control the environment in which a process starts and the mechanisms used for communicating with it. These may be passed wherever you see **options in the documentation below. See the full list or just the frequently used ones in the subprocess documentation. (You may need to import subprocess in order to access constants such as PIPE or DEVNULL.)

Currently, Trio always uses unbuffered byte streams for communicating with a process, so it does not support the encoding, errors, universal_newlines (alias text), and bufsize options.

Quoting: more than you wanted to know

The command to run and its arguments usually must be passed to Trio’s subprocess APIs as a sequence of strings, where the first element in the sequence specifies the command to run and the remaining elements specify its arguments, one argument per element. This form is used because it avoids potential quoting pitfalls; for example, you can run ["cp", "-f", source_file, dest_file] without worrying about whether source_file or dest_file contains spaces.

If you only run subprocesses without shell=True and on UNIX, that’s all you need to know about specifying the command. If you use shell=True or run on Windows, you probably should read the rest of this section to be aware of potential pitfalls.

With shell=True on UNIX, you must specify the command as a single string, which will be passed to the shell as if you’d entered it at an interactive prompt. The advantage of this option is that it lets you use shell features like pipes and redirection without writing code to handle them. For example, you can write Process("ls | grep some_string", shell=True). The disadvantage is that you must account for the shell’s quoting rules, generally by wrapping in shlex.quote() any argument that might contain spaces, quotes, or other shell metacharacters. If you don’t do that, your safe-looking f"ls | grep {some_string}" might end in disaster when invoked with some_string = "foo; rm -rf /".

On Windows, the fundamental API for process spawning (the CreateProcess() system call) takes a string, not a list, and it’s actually up to the child process to decide how it wants to split that string into individual arguments. Since the C language specifies that main() should take a list of arguments, most programs you encounter will follow the rules used by the Microsoft C/C++ runtime. subprocess.Popen, and thus also Trio, uses these rules when it converts an argument sequence to a string, and they are documented alongside the subprocess module. There is no documented Python standard library function that can directly perform that conversion, so even on Windows, you almost always want to pass an argument sequence rather than a string. But if the program you’re spawning doesn’t split its command line back into individual arguments in the standard way, you might need to pass a string to work around this. (Or you might just be out of luck: as far as I can tell, there’s simply no way to pass an argument containing a double-quote to a Windows batch file.)

On Windows with shell=True, things get even more chaotic. Now there are two separate sets of quoting rules applied, one by the Windows command shell CMD.EXE and one by the process being spawned, and they’re different. (And there’s no shlex.quote() to save you: it uses UNIX-style quoting rules, even on Windows.) Most special characters interpreted by the shell &<>()^| are not treated as special if the shell thinks they’re inside double quotes, but %FOO% environment variable substitutions still are, and the shell doesn’t provide any way to write a double quote inside a double-quoted string. Outside double quotes, any character (including a double quote) can be escaped using a leading ^. But since a pipeline is processed by running each command in the pipeline in a subshell, multiple layers of escaping can be needed:

echo ^^^&x | find "x" | find "x"          # prints: &x

And if you combine pipelines with () grouping, you can need even more levels of escaping:

(echo ^^^^^^^&x | find "x") | find "x"    # prints: &x

Since process creation takes a single arguments string, CMD.EXE's quoting does not influence word splitting, and double quotes are not removed during CMD.EXE’s expansion pass. Double quotes are troublesome because CMD.EXE handles them differently from the MSVC runtime rules; in:

prog.exe "foo \"bar\" baz"

the program will see one argument foo "bar" baz but CMD.EXE thinks bar\ is not quoted while foo \ and baz are. All of this makes it a formidable task to reliably interpolate anything into a shell=True command line on Windows, and Trio falls back on the subprocess behavior: If you pass a sequence with shell=True, it’s quoted in the same way as a sequence with shell=False, and had better not contain any shell metacharacters you weren’t planning on.

Further reading:

Signals

with trio.open_signal_receiver(*signals) as signal_aiter

A context manager for catching signals.

Entering this context manager starts listening for the given signals and returns an async iterator; exiting the context manager stops listening.

The async iterator blocks until a signal arrives, and then yields it.

Note that if you leave the with block while the iterator has unextracted signals still pending inside it, then they will be re-delivered using Python’s regular signal handling logic. This avoids a race condition when signals arrives just before we exit the with block.

Parameters

signals – the signals to listen for.

Raises
  • TypeError – if no signals were provided.

  • RuntimeError – if you try to use this anywhere except Python’s main thread. (This is a Python limitation.)

Example

A common convention for Unix daemons is that they should reload their configuration when they receive a SIGHUP. Here’s a sketch of what that might look like using open_signal_receiver():

with trio.open_signal_receiver(signal.SIGHUP) as signal_aiter:
    async for signum in signal_aiter:
        assert signum == signal.SIGHUP
        reload_configuration()

Testing made easier with trio.testing

The trio.testing module provides various utilities to make it easier to test Trio code. Unlike the other submodules in the trio namespace, trio.testing is not automatically imported when you do import trio; you must import trio.testing explicitly.

Test harness integration

@trio.testing.trio_test

Time and timeouts

trio.testing.MockClock is a Clock with a few tricks up its sleeve to help you efficiently test code involving timeouts:

  • By default, it starts at time 0, and clock time only advances when you explicitly call jump(). This provides an extremely controllable clock for testing.

  • You can set rate to 1.0 if you want it to start running in real time like a regular clock. You can stop and start the clock within a test. You can set rate to 10.0 to make clock time pass at 10x real speed (so e.g. await trio.sleep(10) returns after 1 second).

  • But even more interestingly, you can set autojump_threshold to zero or a small value, and then it will watch the execution of the run loop, and any time things have settled down and everyone’s waiting for a timeout, it jumps the clock forward to that timeout. In many cases this allows natural-looking code involving timeouts to be automatically run at near full CPU utilization with no changes. (Thanks to fluxcapacitor for this awesome idea.)

  • And of course these can be mixed and matched at will.

Regardless of these shenanigans, from “inside” Trio the passage of time still seems normal so long as you restrict yourself to Trio’s time functions (see Time and clocks). Below is an example demonstrating two different ways of making time pass quickly. Notice how in both cases, the two tasks keep a consistent view of reality and events happen in the expected order, despite being wildly divorced from real time:

# across-realtime.py

import time
import trio
import trio.testing

YEAR = 365 * 24 * 60 * 60  # seconds

async def task1():
    start = trio.current_time()

    print("task1: sleeping for 1 year")
    await trio.sleep(YEAR)

    duration = trio.current_time() - start
    print("task1: woke up; clock says I've slept {} years"
          .format(duration / YEAR))

    print("task1: sleeping for 1 year, 100 times")
    for _ in range(100):
        await trio.sleep(YEAR)

    duration = trio.current_time() - start
    print("task1: slept {} years total".format(duration / YEAR))

async def task2():
    start = trio.current_time()

    print("task2: sleeping for 5 years")
    await trio.sleep(5 * YEAR)

    duration = trio.current_time() - start
    print("task2: woke up; clock says I've slept {} years"
          .format(duration / YEAR))

    print("task2: sleeping for 500 years")
    await trio.sleep(500 * YEAR)

    duration = trio.current_time() - start
    print("task2: slept {} years total".format(duration / YEAR))

async def main():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(task1)
        nursery.start_soon(task2)

def run_example(clock):
    real_start = time.perf_counter()
    trio.run(main, clock=clock)
    real_duration = time.perf_counter() - real_start
    print("Total real time elapsed: {} seconds".format(real_duration))

print("Clock where time passes at 100 years per second:\n")
run_example(trio.testing.MockClock(rate=100 * YEAR))

print("\nClock where time automatically skips past the boring parts:\n")
run_example(trio.testing.MockClock(autojump_threshold=0))

Output:

Clock where time passes at 100 years per second:

task2: sleeping for 5 years
task1: sleeping for 1 year
task1: woke up; clock says I've slept 1.0365006048232317 years
task1: sleeping for 1 year, 100 times
task2: woke up; clock says I've slept 5.0572111969813704 years
task2: sleeping for 500 years
task1: slept 104.77677842136472 years total
task2: slept 505.25014589075 years total
Total real time elapsed: 5.053582429885864 seconds

Clock where time automatically skips past the boring parts:

task2: sleeping for 5 years
task1: sleeping for 1 year
task1: woke up; clock says I've slept 1.0 years
task1: sleeping for 1 year, 100 times
task2: woke up; clock says I've slept 5.0 years
task2: sleeping for 500 years
task1: slept 101.0 years total
task2: slept 505.0 years total
Total real time elapsed: 0.019298791885375977 seconds
class trio.testing.MockClock(rate=0.0, autojump_threshold=inf)

A user-controllable clock suitable for writing tests.

Parameters
rate

How many seconds of clock time pass per second of real time. Default is 0.0, i.e. the clock only advances through manuals calls to jump() or when the autojump_threshold is triggered. You can assign to this attribute to change it.

autojump_threshold

The clock keeps an eye on the run loop, and if at any point it detects that all tasks have been blocked for this many real seconds (i.e., according to the actual clock, not this clock), then the clock automatically jumps ahead to the run loop’s next scheduled timeout. Default is math.inf, i.e., to never autojump. You can assign to this attribute to change it.

Basically the idea is that if you have code or tests that use sleeps and timeouts, you can use this to make it run much faster, totally automatically. (At least, as long as those sleeps/timeouts are happening inside Trio; if your test involves talking to external service and waiting for it to timeout then obviously we can’t help you there.)

You should set this to the smallest value that lets you reliably avoid “false alarms” where some I/O is in flight (e.g. between two halves of a socketpair) but the threshold gets triggered and time gets advanced anyway. This will depend on the details of your tests and test environment. If you aren’t doing any I/O (like in our sleeping example above) then just set it to zero, and the clock will jump whenever all tasks are blocked.

Note

If you use autojump_threshold and wait_all_tasks_blocked at the same time, then you might wonder how they interact, since they both cause things to happen after the run loop goes idle for some time. The answer is: wait_all_tasks_blocked takes priority. If there’s a task blocked in wait_all_tasks_blocked, then the autojump feature treats that as active task and does not jump the clock.

jump(seconds)

Manually advance the clock by the given number of seconds.

Parameters

seconds (float) – the number of seconds to jump the clock forward.

Raises

ValueError – if you try to pass a negative value for seconds.

Inter-task ordering

class trio.testing.Sequencer

A convenience class for forcing code in different tasks to run in an explicit linear order.

Instances of this class implement a __call__ method which returns an async context manager. The idea is that you pass a sequence number to __call__ to say where this block of code should go in the linear sequence. Block 0 starts immediately, and then block N doesn’t start until block N-1 has finished.

Example

An extremely elaborate way to print the numbers 0-5, in order:

async def worker1(seq):
    async with seq(0):
        print(0)
    async with seq(4):
        print(4)

async def worker2(seq):
    async with seq(2):
        print(2)
    async with seq(5):
        print(5)

async def worker3(seq):
    async with seq(1):
        print(1)
    async with seq(3):
        print(3)

async def main():
   seq = trio.testing.Sequencer()
   async with trio.open_nursery() as nursery:
       nursery.start_soon(worker1, seq)
       nursery.start_soon(worker2, seq)
       nursery.start_soon(worker3, seq)
await trio.testing.wait_all_tasks_blocked(cushion=0.0)

Block until there are no runnable tasks.

This is useful in testing code when you want to give other tasks a chance to “settle down”. The calling task is blocked, and doesn’t wake up until all other tasks are also blocked for at least cushion seconds. (Setting a non-zero cushion is intended to handle cases like two tasks talking to each other over a local socket, where we want to ignore the potential brief moment between a send and receive when all tasks are blocked.)

Note that cushion is measured in real time, not the Trio clock time.

If there are multiple tasks blocked in wait_all_tasks_blocked(), then the one with the shortest cushion is the one woken (and this task becoming unblocked resets the timers for the remaining tasks). If there are multiple tasks that have exactly the same cushion, then all are woken.

You should also consider trio.testing.Sequencer, which provides a more explicit way to control execution ordering within a test, and will often produce more readable tests.

Example

Here’s an example of one way to test that Trio’s locks are fair: we take the lock in the parent, start a child, wait for the child to be blocked waiting for the lock (!), and then check that we can’t release and immediately re-acquire the lock:

async def lock_taker(lock):
    await lock.acquire()
    lock.release()

async def test_lock_fairness():
    lock = trio.Lock()
    await lock.acquire()
    async with trio.open_nursery() as nursery:
        nursery.start_soon(lock_taker, lock)
        # child hasn't run yet, we have the lock
        assert lock.locked()
        assert lock._owner is trio.lowlevel.current_task()
        await trio.testing.wait_all_tasks_blocked()
        # now the child has run and is blocked on lock.acquire(), we
        # still have the lock
        assert lock.locked()
        assert lock._owner is trio.lowlevel.current_task()
        lock.release()
        try:
            # The child has a prior claim, so we can't have it
            lock.acquire_nowait()
        except trio.WouldBlock:
            assert lock._owner is not trio.lowlevel.current_task()
            print("PASS")
        else:
            print("FAIL")

Streams

Connecting to an in-process socket server

await trio.testing.open_stream_to_socket_listener(socket_listener)

Connect to the given SocketListener.

This is particularly useful in tests when you want to let a server pick its own port, and then connect to it:

listeners = await trio.open_tcp_listeners(0)
client = await trio.testing.open_stream_to_socket_listener(listeners[0])
Parameters

socket_listener (SocketListener) – The SocketListener to connect to.

Returns

a stream connected to the given listener.

Return type

SocketStream

Virtual, controllable streams

One particularly challenging problem when testing network protocols is making sure that your implementation can handle data whose flow gets broken up in weird ways and arrives with weird timings: localhost connections tend to be much better behaved than real networks, so if you only test on localhost then you might get bitten later. To help you out, Trio provides some fully in-memory implementations of the stream interfaces (see The abstract Stream API), that let you write all kinds of interestingly evil tests.

There are a few pieces here, so here’s how they fit together:

memory_stream_pair() gives you a pair of connected, bidirectional streams. It’s like socket.socketpair(), but without any involvement from that pesky operating system and its networking stack.

To build a bidirectional stream, memory_stream_pair() uses two unidirectional streams. It gets these by calling memory_stream_one_way_pair().

memory_stream_one_way_pair(), in turn, is implemented using the low-ish level classes MemorySendStream and MemoryReceiveStream. These are implementations of (you guessed it) trio.abc.SendStream and trio.abc.ReceiveStream that on their own, aren’t attached to anything – “sending” and “receiving” just put data into and get data out of a private internal buffer that each object owns. They also have some interesting hooks you can set, that let you customize the behavior of their methods. This is where you can insert the evil, if you want it. memory_stream_one_way_pair() takes advantage of these hooks in a relatively boring way: it just sets it up so that when you call send_all, or when you close the send stream, then it automatically triggers a call to memory_stream_pump(), which is a convenience function that takes data out of a MemorySendStream´s buffer and puts it into a MemoryReceiveStream´s buffer. But that’s just the default – you can replace this with whatever arbitrary behavior you want.

Trio also provides some specialized functions for testing completely unbuffered streams: lockstep_stream_one_way_pair() and lockstep_stream_pair(). These aren’t customizable, but they do exhibit an extreme kind of behavior that’s good at catching out edge cases in protocol implementations.

API details

class trio.testing.MemorySendStream(send_all_hook=None, wait_send_all_might_not_block_hook=None, close_hook=None)

An in-memory SendStream.

Parameters
  • send_all_hook – An async function, or None. Called from send_all(). Can do whatever you like.

  • wait_send_all_might_not_block_hook – An async function, or None. Called from wait_send_all_might_not_block(). Can do whatever you like.

  • close_hook – A synchronous function, or None. Called from close() and aclose(). Can do whatever you like.

send_all_hook
wait_send_all_might_not_block_hook
close_hook

All of these hooks are also exposed as attributes on the object, and you can change them at any time.

await aclose()

Same as close(), but async.

close()

Marks this stream as closed, and then calls the close_hook (if any).

await get_data(max_bytes=None)

Retrieves data from the internal buffer, blocking if necessary.

Parameters

max_bytes (int or None) – The maximum amount of data to retrieve. None (the default) means to retrieve all the data that’s present (but still blocks until at least one byte is available).

Returns

If this stream has been closed, an empty bytearray. Otherwise, the requested data.

get_data_nowait(max_bytes=None)

Retrieves data from the internal buffer, but doesn’t block.

See get_data() for details.

Raises

trio.WouldBlock – if no data is available to retrieve.

await send_all(data)

Places the given data into the object’s internal buffer, and then calls the send_all_hook (if any).

await wait_send_all_might_not_block()

Calls the wait_send_all_might_not_block_hook (if any), and then returns immediately.

class trio.testing.MemoryReceiveStream(receive_some_hook=None, close_hook=None)

An in-memory ReceiveStream.

Parameters
  • receive_some_hook – An async function, or None. Called from receive_some(). Can do whatever you like.

  • close_hook – A synchronous function, or None. Called from close() and aclose(). Can do whatever you like.

receive_some_hook
close_hook

Both hooks are also exposed as attributes on the object, and you can change them at any time.

await aclose()

Same as close(), but async.

close()

Discards any pending data from the internal buffer, and marks this stream as closed.

put_data(data)

Appends the given data to the internal buffer.

put_eof()

Adds an end-of-file marker to the internal buffer.

await receive_some(max_bytes=None)

Calls the receive_some_hook (if any), and then retrieves data from the internal buffer, blocking if necessary.

trio.testing.memory_stream_pump(memory_send_stream, memory_receive_stream, *, max_bytes=None)

Take data out of the given MemorySendStream’s internal buffer, and put it into the given MemoryReceiveStream’s internal buffer.

Parameters
  • memory_send_stream (MemorySendStream) – The stream to get data from.

  • memory_receive_stream (MemoryReceiveStream) – The stream to put data into.

  • max_bytes (int or None) – The maximum amount of data to transfer in this call, or None to transfer all available data.

Returns

True if it successfully transferred some data, or False if there was no data to transfer.

This is used to implement memory_stream_one_way_pair() and memory_stream_pair(); see the latter’s docstring for an example of how you might use it yourself.

trio.testing.memory_stream_one_way_pair()

Create a connected, pure-Python, unidirectional stream with infinite buffering and flexible configuration options.

You can think of this as being a no-operating-system-involved Trio-streamsified version of os.pipe() (except that os.pipe() returns the streams in the wrong order – we follow the superior convention that data flows from left to right).

Returns

A tuple (MemorySendStream, MemoryReceiveStream), where the MemorySendStream has its hooks set up so that it calls memory_stream_pump() from its send_all_hook and close_hook.

The end result is that data automatically flows from the MemorySendStream to the MemoryReceiveStream. But you’re also free to rearrange things however you like. For example, you can temporarily set the send_all_hook to None if you want to simulate a stall in data transmission. Or see memory_stream_pair() for a more elaborate example.

trio.testing.memory_stream_pair()

Create a connected, pure-Python, bidirectional stream with infinite buffering and flexible configuration options.

This is a convenience function that creates two one-way streams using memory_stream_one_way_pair(), and then uses StapledStream to combine them into a single bidirectional stream.

This is like a no-operating-system-involved, Trio-streamsified version of socket.socketpair().

Returns

A pair of StapledStream objects that are connected so that data automatically flows from one to the other in both directions.

After creating a stream pair, you can send data back and forth, which is enough for simple tests:

left, right = memory_stream_pair()
await left.send_all(b"123")
assert await right.receive_some() == b"123"
await right.send_all(b"456")
assert await left.receive_some() == b"456"

But if you read the docs for StapledStream and memory_stream_one_way_pair(), you’ll see that all the pieces involved in wiring this up are public APIs, so you can adjust to suit the requirements of your tests. For example, here’s how to tweak a stream so that data flowing from left to right trickles in one byte at a time (but data flowing from right to left proceeds at full speed):

left, right = memory_stream_pair()
async def trickle():
    # left is a StapledStream, and left.send_stream is a MemorySendStream
    # right is a StapledStream, and right.recv_stream is a MemoryReceiveStream
    while memory_stream_pump(left.send_stream, right.recv_stream, max_bytes=1):
        # Pause between each byte
        await trio.sleep(1)
# Normally this send_all_hook calls memory_stream_pump directly without
# passing in a max_bytes. We replace it with our custom version:
left.send_stream.send_all_hook = trickle

And here’s a simple test using our modified stream objects:

async def sender():
    await left.send_all(b"12345")
    await left.send_eof()

async def receiver():
    async for data in right:
        print(data)

async with trio.open_nursery() as nursery:
    nursery.start_soon(sender)
    nursery.start_soon(receiver)

By default, this will print b"12345" and then immediately exit; with our trickle stream it instead sleeps 1 second, then prints b"1", then sleeps 1 second, then prints b"2", etc.

Pro-tip: you can insert sleep calls (like in our example above) to manipulate the flow of data across tasks… and then use MockClock and its autojump_threshold functionality to keep your test suite running quickly.

If you want to stress test a protocol implementation, one nice trick is to use the random module (preferably with a fixed seed) to move random numbers of bytes at a time, and insert random sleeps in between them. You can also set up a custom receive_some_hook if you want to manipulate things on the receiving side, and not just the sending side.

trio.testing.lockstep_stream_one_way_pair()

Create a connected, pure Python, unidirectional stream where data flows in lockstep.

Returns

A tuple (SendStream, ReceiveStream).

This stream has absolutely no buffering. Each call to send_all() will block until all the given data has been returned by a call to receive_some().

This can be useful for testing flow control mechanisms in an extreme case, or for setting up “clogged” streams to use with check_one_way_stream() and friends.

In addition to fulfilling the SendStream and ReceiveStream interfaces, the return objects also have a synchronous close method.

trio.testing.lockstep_stream_pair()

Create a connected, pure-Python, bidirectional stream where data flows in lockstep.

Returns

A tuple (StapledStream, StapledStream).

This is a convenience function that creates two one-way streams using lockstep_stream_one_way_pair(), and then uses StapledStream to combine them into a single bidirectional stream.

Testing custom stream implementations

Trio also provides some functions to help you test your custom stream implementations:

await trio.testing.check_one_way_stream(stream_maker, clogged_stream_maker)

Perform a number of generic tests on a custom one-way stream implementation.

Parameters
  • stream_maker – An async (!) function which returns a connected (SendStream, ReceiveStream) pair.

  • clogged_stream_maker – Either None, or an async function similar to stream_maker, but with the extra property that the returned stream is in a state where send_all and wait_send_all_might_not_block will block until receive_some has been called. This allows for more thorough testing of some edge cases, especially around wait_send_all_might_not_block.

Raises

AssertionError – if a test fails.

await trio.testing.check_two_way_stream(stream_maker, clogged_stream_maker)

Perform a number of generic tests on a custom two-way stream implementation.

This is similar to check_one_way_stream(), except that the maker functions are expected to return objects implementing the Stream interface.

This function tests a superset of what check_one_way_stream() checks – if you call this, then you don’t need to also call check_one_way_stream().

await trio.testing.check_half_closeable_stream(stream_maker, clogged_stream_maker)

Perform a number of generic tests on a custom half-closeable stream implementation.

This is similar to check_two_way_stream(), except that the maker functions are expected to return objects that implement the HalfCloseableStream interface.

This function tests a superset of what check_two_way_stream() checks – if you call this, then you don’t need to also call check_two_way_stream().

Virtual networking for testing

In the previous section you learned how to use virtual in-memory streams to test protocols that are written against Trio’s Stream abstraction. But what if you have more complicated networking code – the kind of code that makes connections to multiple hosts, or opens a listening socket, or sends UDP packets?

Trio doesn’t itself provide a virtual in-memory network implementation for testing – but trio.socket module does provide the hooks you need to write your own! And if you’re interested in helping implement a reusable virtual network for testing, then please get in touch.

Note that these APIs are actually in trio.socket and trio.abc, but we document them here because they’re primarily intended for testing.

trio.socket.set_custom_hostname_resolver(hostname_resolver)

Set a custom hostname resolver.

By default, Trio’s getaddrinfo() and getnameinfo() functions use the standard system resolver functions. This function allows you to customize that behavior. The main intended use case is for testing, but it might also be useful for using third-party resolvers like c-ares (though be warned that these rarely make perfect drop-in replacements for the system resolver). See trio.abc.HostnameResolver for more details.

Setting a custom hostname resolver affects all future calls to getaddrinfo() and getnameinfo() within the enclosing call to trio.run(). All other hostname resolution in Trio is implemented in terms of these functions.

Generally you should call this function just once, right at the beginning of your program.

Parameters

hostname_resolver (trio.abc.HostnameResolver or None) – The new custom hostname resolver, or None to restore the default behavior.

Returns

The previous hostname resolver (which may be None).

class trio.abc.HostnameResolver

If you have a custom hostname resolver, then implementing HostnameResolver allows you to register this to be used by Trio.

See trio.socket.set_custom_hostname_resolver().

abstractmethod await getaddrinfo(host, port, family=0, type=0, proto=0, flags=0)

A custom implementation of getaddrinfo().

Called by trio.socket.getaddrinfo().

If host is given as a numeric IP address, then getaddrinfo() may handle the request itself rather than calling this method.

Any required IDNA encoding is handled before calling this function; your implementation can assume that it will never see U-labels like "café.com", and only needs to handle A-labels like b"xn--caf-dma.com".

abstractmethod await getnameinfo(sockaddr, flags)

A custom implementation of getnameinfo().

Called by trio.socket.getnameinfo().

trio.socket.set_custom_socket_factory(socket_factory)

Set a custom socket object factory.

This function allows you to replace Trio’s normal socket class with a custom class. This is very useful for testing, and probably a bad idea in any other circumstance. See trio.abc.HostnameResolver for more details.

Setting a custom socket factory affects all future calls to socket() within the enclosing call to trio.run().

Generally you should call this function just once, right at the beginning of your program.

Parameters

socket_factory (trio.abc.SocketFactory or None) – The new custom socket factory, or None to restore the default behavior.

Returns

The previous socket factory (which may be None).

class trio.abc.SocketFactory

If you write a custom class implementing the Trio socket interface, then you can use a SocketFactory to get Trio to use it.

See trio.socket.set_custom_socket_factory().

abstractmethod socket(family=None, type=None, proto=None)

Create and return a socket object.

Your socket object must inherit from trio.socket.SocketType, which is an empty class whose only purpose is to “mark” which classes should be considered valid Trio sockets.

Called by trio.socket.socket().

Note that unlike trio.socket.socket(), this does not take a fileno= argument. If a fileno= is specified, then trio.socket.socket() returns a regular Trio socket object instead of calling this method.

Testing checkpoints

with trio.testing.assert_checkpoints()

Use as a context manager to check that the code inside the with block either exits with an exception or executes at least one checkpoint.

Raises

AssertionError – if no checkpoint was executed.

Example

Check that trio.sleep() is a checkpoint, even if it doesn’t block:

with trio.testing.assert_checkpoints():
    await trio.sleep(0)
with trio.testing.assert_no_checkpoints()

Use as a context manager to check that the code inside the with block does not execute any checkpoints.

Raises

AssertionError – if a checkpoint was executed.

Example

Synchronous code never contains any checkpoints, but we can double-check that:

send_channel, receive_channel = trio.open_memory_channel(10)
with trio.testing.assert_no_checkpoints():
    send_channel.send_nowait(None)

Introspecting and extending Trio with trio.lowlevel

trio.lowlevel contains low-level APIs for introspecting and extending Trio. If you’re writing ordinary, everyday code, then you can ignore this module completely. But sometimes you need something a bit lower level. Here are some examples of situations where you should reach for trio.lowlevel:

  • You want to implement a new synchronization primitive that Trio doesn’t (yet) provide, like a reader-writer lock.

  • You want to extract low-level metrics to monitor the health of your application.

  • You want to use a low-level operating system interface that Trio doesn’t (yet) provide its own wrappers for, like watching a filesystem directory for changes.

  • You want to implement an interface for calling between Trio and another event loop within the same process.

  • You’re writing a debugger and want to visualize Trio’s task tree.

  • You need to interoperate with a C library whose API exposes raw file descriptors.

You don’t need to be scared of trio.lowlevel, as long as you take proper precautions. These are real public APIs, with strictly defined and carefully documented semantics. They’re the same tools we use to implement all the nice high-level APIs in the trio namespace. But, be careful. Some of those strict semantics have nasty big pointy teeth. If you make a mistake, Trio may not be able to handle it gracefully; conventions and guarantees that are followed strictly in the rest of Trio do not always apply. When you use this module, it’s your job to think about how you’re going to handle the tricky cases so you can expose a friendly Trio-style API to your users.

Debugging and instrumentation

Trio tries hard to provide useful hooks for debugging and instrumentation. Some are documented above (the nursery introspection attributes, trio.Lock.statistics(), etc.). Here are some more.

Global statistics

trio.lowlevel.current_statistics()

Returns an object containing run-loop-level debugging information.

Currently the following fields are defined:

  • tasks_living (int): The number of tasks that have been spawned and not yet exited.

  • tasks_runnable (int): The number of tasks that are currently queued on the run queue (as opposed to blocked waiting for something to happen).

  • seconds_to_next_deadline (float): The time until the next pending cancel scope deadline. May be negative if the deadline has expired but we haven’t yet processed cancellations. May be inf if there are no pending deadlines.

  • run_sync_soon_queue_size (int): The number of unprocessed callbacks queued via trio.lowlevel.TrioToken.run_sync_soon().

  • io_statistics (object): Some statistics from Trio’s I/O backend. This always has an attribute backend which is a string naming which operating-system-specific I/O backend is in use; the other attributes vary between backends.

The current clock

trio.lowlevel.current_clock()

Returns the current Clock.

Instrument API

The instrument API provides a standard way to add custom instrumentation to the run loop. Want to make a histogram of scheduling latencies, log a stack trace of any task that blocks the run loop for >50 ms, or measure what percentage of your process’s running time is spent waiting for I/O? This is the place.

The general idea is that at any given moment, trio.run() maintains a set of “instruments”, which are objects that implement the trio.abc.Instrument interface. When an interesting event happens, it loops over these instruments and notifies them by calling an appropriate method. The tutorial has a simple example of using this for tracing.

Since this hooks into Trio at a rather low level, you do have to be careful. The callbacks are run synchronously, and in many cases if they error out then there isn’t any plausible way to propagate this exception (for instance, we might be deep in the guts of the exception propagation machinery…). Therefore our current strategy for handling exceptions raised by instruments is to (a) log an exception to the "trio.abc.Instrument" logger, which by default prints a stack trace to standard error and (b) disable the offending instrument.

You can register an initial list of instruments by passing them to trio.run(). add_instrument() and remove_instrument() let you add and remove instruments at runtime.

trio.lowlevel.add_instrument(instrument: trio.abc.Instrument)None

Start instrumenting the current run loop with the given instrument.

Parameters

instrument (trio.abc.Instrument) – The instrument to activate.

If instrument is already active, does nothing.

trio.lowlevel.remove_instrument(instrument: trio.abc.Instrument)None

Stop instrumenting the current run loop with the given instrument.

Parameters

instrument (trio.abc.Instrument) – The instrument to de-activate.

Raises

KeyError – if the instrument is not currently active. This could occur either because you never added it, or because you added it and then it raised an unhandled exception and was automatically deactivated.

And here’s the interface to implement if you want to build your own Instrument:

class trio.abc.Instrument

The interface for run loop instrumentation.

Instruments don’t have to inherit from this abstract base class, and all of these methods are optional. This class serves mostly as documentation.

after_io_wait(timeout)

Called after handling pending I/O.

Parameters

timeout (float) – The number of seconds we were willing to wait. This much time may or may not have elapsed, depending on whether any I/O was ready.

after_run()

Called just before trio.run() returns.

after_task_step(task)

Called when we return to the main run loop after a task has yielded.

Parameters

task (trio.lowlevel.Task) – The task that just ran.

before_io_wait(timeout)

Called before blocking to wait for I/O readiness.

Parameters

timeout (float) – The number of seconds we are willing to wait.

before_run()

Called at the beginning of trio.run().

before_task_step(task)

Called immediately before we resume running the given task.

Parameters

task (trio.lowlevel.Task) – The task that is about to run.

task_exited(task)

Called when the given task exits.

Parameters

task (trio.lowlevel.Task) – The finished task.

task_scheduled(task)

Called when the given task becomes runnable.

It may still be some time before it actually runs, if there are other runnable tasks ahead of it.

Parameters

task (trio.lowlevel.Task) – The task that became runnable.

task_spawned(task)

Called when the given task is created.

Parameters

task (trio.lowlevel.Task) – The new task.

The tutorial has a fully-worked example of defining a custom instrument to log Trio’s internal scheduling decisions.

Low-level process spawning

await trio.lowlevel.open_process(command, *, stdin=None, stdout=None, stderr=None, **options)trio.Process

Execute a child program in a new process.

After construction, you can interact with the child process by writing data to its stdin stream (a SendStream), reading data from its stdout and/or stderr streams (both ReceiveStreams), sending it signals using terminate, kill, or send_signal, and waiting for it to exit using wait. See trio.Process for details.

Each standard stream is only available if you specify that a pipe should be created for it. For example, if you pass stdin=subprocess.PIPE, you can write to the stdin stream, else stdin will be None.

Unlike trio.run_process, this function doesn’t do any kind of automatic management of the child process. It’s up to you to implement whatever semantics you want.

Parameters
  • command (list or str) – The command to run. Typically this is a sequence of strings such as ['ls', '-l', 'directory with spaces'], where the first element names the executable to invoke and the other elements specify its arguments. With shell=True in the **options, or on Windows, command may alternatively be a string, which will be parsed following platform-dependent quoting rules.

  • stdin – Specifies what the child process’s standard input stream should connect to: output written by the parent (subprocess.PIPE), nothing (subprocess.DEVNULL), or an open file (pass a file descriptor or something whose fileno method returns one). If stdin is unspecified, the child process will have the same standard input stream as its parent.

  • stdout – Like stdin, but for the child process’s standard output stream.

  • stderr – Like stdin, but for the child process’s standard error stream. An additional value subprocess.STDOUT is supported, which causes the child’s standard output and standard error messages to be intermixed on a single standard output stream, attached to whatever the stdout option says to attach it to.

  • **options – Other general subprocess options are also accepted.

Returns

A new trio.Process object.

Raises

OSError – if the process spawning fails, for example because the specified command could not be found.

Low-level I/O primitives

Different environments expose different low-level APIs for performing async I/O. trio.lowlevel exposes these APIs in a relatively direct way, so as to allow maximum power and flexibility for higher level code. However, this means that the exact API provided may vary depending on what system Trio is running on.

Universally available API

All environments provide the following functions:

await trio.lowlevel.wait_readable(obj)

Block until the kernel reports that the given object is readable.

On Unix systems, obj must either be an integer file descriptor, or else an object with a .fileno() method which returns an integer file descriptor. Any kind of file descriptor can be passed, though the exact semantics will depend on your kernel. For example, this probably won’t do anything useful for on-disk files.

On Windows systems, obj must either be an integer SOCKET handle, or else an object with a .fileno() method which returns an integer SOCKET handle. File descriptors aren’t supported, and neither are handles that refer to anything besides a SOCKET.

Raises
await trio.lowlevel.wait_writable(obj)

Block until the kernel reports that the given object is writable.

See wait_readable for the definition of obj.

Raises
trio.lowlevel.notify_closing(obj)

Call this before closing a file descriptor (on Unix) or socket (on Windows). This will cause any wait_readable or wait_writable calls on the given object to immediately wake up and raise ClosedResourceError.

This doesn’t actually close the object – you still have to do that yourself afterwards. Also, you want to be careful to make sure no new tasks start waiting on the object in between when you call this and when it’s actually closed. So to close something properly, you usually want to do these steps in order:

  1. Explicitly mark the object as closed, so that any new attempts to use it will abort before they start.

  2. Call notify_closing to wake up any already-existing users.

  3. Actually close the object.

It’s also possible to do them in a different order if that’s more convenient, but only if you make sure not to have any checkpoints in between the steps. This way they all happen in a single atomic step, so other tasks won’t be able to tell what order they happened in anyway.

Unix-specific API

FdStream supports wrapping Unix files (such as a pipe or TTY) as a stream.

If you have two different file descriptors for sending and receiving, and want to bundle them together into a single bidirectional Stream, then use trio.StapledStream:

bidirectional_stream = trio.StapledStream(
    trio.lowlevel.FdStream(write_fd),
    trio.lowlevel.FdStream(read_fd)
)
class trio.lowlevel.FdStream(fd: int)

Bases: trio.abc.Stream

Represents a stream given the file descriptor to a pipe, TTY, etc.

fd must refer to a file that is open for reading and/or writing and supports non-blocking I/O (pipes and TTYs will work, on-disk files probably not). The returned stream takes ownership of the fd, so closing the stream will close the fd too. As with os.fdopen, you should not directly use an fd after you have wrapped it in a stream using this function.

To be used as a Trio stream, an open file must be placed in non-blocking mode. Unfortunately, this impacts all I/O that goes through the underlying open file, including I/O that uses a different file descriptor than the one that was passed to Trio. If other threads or processes are using file descriptors that are related through os.dup or inheritance across os.fork to the one that Trio is using, they are unlikely to be prepared to have non-blocking I/O semantics suddenly thrust upon them. For example, you can use FdStream(os.dup(sys.stdin.fileno())) to obtain a stream for reading from standard input, but it is only safe to do so with heavy caveats: your stdin must not be shared by any other processes and you must not make any calls to synchronous methods of sys.stdin until the stream returned by FdStream is closed. See issue #174 for a discussion of the challenges involved in relaxing this restriction.

Parameters

fd (int) – The fd to be wrapped.

Returns

A new FdStream object.

Kqueue-specific API

TODO: these are implemented, but are currently more of a sketch than anything real. See #26.

trio.lowlevel.current_kqueue()
await trio.lowlevel.wait_kevent(ident, filter, abort_func)
with trio.lowlevel.monitor_kevent(ident, filter) as queue

Windows-specific API

await trio.lowlevel.WaitForSingleObject(handle)

Async and cancellable variant of WaitForSingleObject. Windows only.

Parameters

handle – A Win32 object handle, as a Python integer.

Raises

OSError – If the handle is invalid, e.g. when it is already closed.

TODO: these are implemented, but are currently more of a sketch than anything real. See #26 and #52.

trio.lowlevel.register_with_iocp(handle)
await trio.lowlevel.wait_overlapped(handle, lpOverlapped)
trio.lowlevel.current_iocp()
with trio.lowlevel.monitor_completion_key() as queue

Global state: system tasks and run-local variables

class trio.lowlevel.RunVar(name, default=<object object>)

The run-local variant of a context variable.

RunVar objects are similar to context variable objects, except that they are shared across a single call to trio.run() rather than a single task.

trio.lowlevel.spawn_system_task(async_fn, *args, name=None, context=None)

Spawn a “system” task.

System tasks have a few differences from regular tasks:

  • They don’t need an explicit nursery; instead they go into the internal “system nursery”.

  • If a system task raises an exception, then it’s converted into a TrioInternalError and all tasks are cancelled. If you write a system task, you should be careful to make sure it doesn’t crash.

  • System tasks are automatically cancelled when the main task exits.

  • By default, system tasks have KeyboardInterrupt protection enabled. If you want your task to be interruptible by control-C, then you need to use disable_ki_protection() explicitly (and come up with some plan for what to do with a KeyboardInterrupt, given that system tasks aren’t allowed to raise exceptions).

  • System tasks do not inherit context variables from their creator.

Towards the end of a call to trio.run(), after the main task and all system tasks have exited, the system nursery becomes closed. At this point, new calls to spawn_system_task() will raise RuntimeError("Nursery is closed to new arrivals") instead of creating a system task. It’s possible to encounter this state either in a finally block in an async generator, or in a callback passed to TrioToken.run_sync_soon() at the right moment.

Parameters
  • async_fn – An async callable.

  • args – Positional arguments for async_fn. If you want to pass keyword arguments, use functools.partial().

  • name – The name for this task. Only used for debugging/introspection (e.g. repr(task_obj)). If this isn’t a string, spawn_system_task() will try to make it one. A common use case is if you’re wrapping a function before spawning a new task, you might pass the original function as the name= to make debugging easier.

  • context – An optional contextvars.Context object with context variables to use for this task. You would normally get a copy of the current context with context = contextvars.copy_context() and then you would pass that context object here.

Returns

the newly spawned task

Return type

Task

Trio tokens

class trio.lowlevel.TrioToken

An opaque object representing a single call to trio.run().

It has no public constructor; instead, see current_trio_token().

This object has two uses:

  1. It lets you re-enter the Trio run loop from external threads or signal handlers. This is the low-level primitive that trio.to_thread() and trio.from_thread use to communicate with worker threads, that trio.open_signal_receiver uses to receive notifications about signals, and so forth.

  2. Each call to trio.run() has exactly one associated TrioToken object, so you can use it to identify a particular call.

run_sync_soon(sync_fn, *args, idempotent=False)

Schedule a call to sync_fn(*args) to occur in the context of a Trio task.

This is safe to call from the main thread, from other threads, and from signal handlers. This is the fundamental primitive used to re-enter the Trio run loop from outside of it.

The call will happen “soon”, but there’s no guarantee about exactly when, and no mechanism provided for finding out when it’s happened. If you need this, you’ll have to build your own.

The call is effectively run as part of a system task (see spawn_system_task()). In particular this means that:

  • KeyboardInterrupt protection is enabled by default; if you want sync_fn to be interruptible by control-C, then you need to use disable_ki_protection() explicitly.

  • If sync_fn raises an exception, then it’s converted into a TrioInternalError and all tasks are cancelled. You should be careful that sync_fn doesn’t crash.

All calls with idempotent=False are processed in strict first-in first-out order.

If idempotent=True, then sync_fn and args must be hashable, and Trio will make a best-effort attempt to discard any call submission which is equal to an already-pending call. Trio will process these in first-in first-out order.

Any ordering guarantees apply separately to idempotent=False and idempotent=True calls; there’s no rule for how calls in the different categories are ordered with respect to each other.

Raises

trio.RunFinishedError – if the associated call to trio.run() has already exited. (Any call that doesn’t raise this error is guaranteed to be fully processed before trio.run() exits.)

trio.lowlevel.current_trio_token()

Retrieve the TrioToken for the current call to trio.run().

Spawning threads

trio.lowlevel.start_thread_soon(fn, deliver)

Runs deliver(outcome.capture(fn)) in a worker thread.

Generally fn does some blocking work, and deliver delivers the result back to whoever is interested.

This is a low-level, no-frills interface, very similar to using threading.Thread to spawn a thread directly. The main difference is that this function tries to re-use threads when possible, so it can be a bit faster than threading.Thread.

Worker threads have the daemon flag set, which means that if your main thread exits, worker threads will automatically be killed. If you want to make sure that your fn runs to completion, then you should make sure that the main thread remains alive until deliver is called.

It is safe to call this function simultaneously from multiple threads.

Parameters
  • fn (sync function) – Performs arbitrary blocking work.

  • deliver (sync function) – Takes the outcome.Outcome of fn, and delivers it. Must not block.

Because worker threads are cached and reused for multiple calls, neither function should mutate thread-level state, like threading.local objects – or if they do, they should be careful to revert their changes before returning.

Note

The split between fn and deliver serves two purposes. First, it’s convenient, since most callers need something like this anyway.

Second, it avoids a small race condition that could cause too many threads to be spawned. Consider a program that wants to run several jobs sequentially on a thread, so the main thread submits a job, waits for it to finish, submits another job, etc. In theory, this program should only need one worker thread. But what could happen is:

  1. Worker thread: First job finishes, and calls deliver.

  2. Main thread: receives notification that the job finished, and calls start_thread_soon.

  3. Main thread: sees that no worker threads are marked idle, so spawns a second worker thread.

  4. Original worker thread: marks itself as idle.

To avoid this, threads mark themselves as idle before calling deliver.

Is this potential extra thread a major problem? Maybe not, but it’s easy enough to avoid, and we figure that if the user is trying to limit how many threads they’re using then it’s polite to respect that.

Safer KeyboardInterrupt handling

Trio’s handling of control-C is designed to balance usability and safety. On the one hand, there are sensitive regions (like the core scheduling loop) where it’s simply impossible to handle arbitrary KeyboardInterrupt exceptions while maintaining our core correctness invariants. On the other, if the user accidentally writes an infinite loop, we do want to be able to break out of that. Our solution is to install a default signal handler which checks whether it’s safe to raise KeyboardInterrupt at the place where the signal is received. If so, then we do; otherwise, we schedule a KeyboardInterrupt to be delivered to the main task at the next available opportunity (similar to how Cancelled is delivered).

So that’s great, but – how do we know whether we’re in one of the sensitive parts of the program or not?

This is determined on a function-by-function basis. By default:

  • The top-level function in regular user tasks is unprotected.

  • The top-level function in system tasks is protected.

  • If a function doesn’t specify otherwise, then it inherits the protection state of its caller.

This means you only need to override the defaults at places where you transition from protected code to unprotected code or vice-versa.

These transitions are accomplished using two function decorators:

@trio.lowlevel.disable_ki_protection

Decorator that marks the given regular function, generator function, async function, or async generator function as unprotected against KeyboardInterrupt, i.e., the code inside this function can be rudely interrupted by KeyboardInterrupt at any moment.

If you have multiple decorators on the same function, then this should be at the bottom of the stack (closest to the actual function).

An example of where you’d use this is in implementing something like trio.from_thread.run(), which uses TrioToken.run_sync_soon() to get into the Trio thread. run_sync_soon() callbacks are run with KeyboardInterrupt protection enabled, and trio.from_thread.run() takes advantage of this to safely set up the machinery for sending a response back to the original thread, but then uses disable_ki_protection() when entering the user-provided function.

@trio.lowlevel.enable_ki_protection

Decorator that marks the given regular function, generator function, async function, or async generator function as protected against KeyboardInterrupt, i.e., the code inside this function won’t be rudely interrupted by KeyboardInterrupt. (Though if it contains any checkpoints, then it can still receive KeyboardInterrupt at those. This is considered a polite interruption.)

Warning

Be very careful to only use this decorator on functions that you know will either exit in bounded time, or else pass through a checkpoint regularly. (Of course all of your functions should have this property, but if you mess it up here then you won’t even be able to use control-C to escape!)

If you have multiple decorators on the same function, then this should be at the bottom of the stack (closest to the actual function).

An example of where you’d use this is on the __exit__ implementation for something like a Lock, where a poorly-timed KeyboardInterrupt could leave the lock in an inconsistent state and cause a deadlock.

trio.lowlevel.currently_ki_protected()

Check whether the calling code has KeyboardInterrupt protection enabled.

It’s surprisingly easy to think that one’s KeyboardInterrupt protection is enabled when it isn’t, or vice-versa. This function tells you what Trio thinks of the matter, which makes it useful for asserts and unit tests.

Returns

True if protection is enabled, and False otherwise.

Return type

bool

Sleeping and waking

Wait queue abstraction

class trio.lowlevel.ParkingLot

A fair wait queue with cancellation and requeueing.

This class encapsulates the tricky parts of implementing a wait queue. It’s useful for implementing higher-level synchronization primitives like queues and locks.

In addition to the methods below, you can use len(parking_lot) to get the number of parked tasks, and if parking_lot: ... to check whether there are any parked tasks.

await park()

Park the current task until woken by a call to unpark() or unpark_all().

repark(new_lot, *, count=1)

Move parked tasks from one ParkingLot object to another.

This dequeues count tasks from one lot, and requeues them on another, preserving order. For example:

async def parker(lot):
    print("sleeping")
    await lot.park()
    print("woken")

async def main():
    lot1 = trio.lowlevel.ParkingLot()
    lot2 = trio.lowlevel.ParkingLot()
    async with trio.open_nursery() as nursery:
        nursery.start_soon(parker, lot1)
        await trio.testing.wait_all_tasks_blocked()
        assert len(lot1) == 1
        assert len(lot2) == 0
        lot1.repark(lot2)
        assert len(lot1) == 0
        assert len(lot2) == 1
        # This wakes up the task that was originally parked in lot1
        lot2.unpark()

If there are fewer than count tasks parked, then reparks as many tasks as are available and then returns successfully.

Parameters
  • new_lot (ParkingLot) – the parking lot to move tasks to.

  • count (int) – the number of tasks to move.

repark_all(new_lot)

Move all parked tasks from one ParkingLot object to another.

See repark() for details.

statistics()

Return an object containing debugging information.

Currently the following fields are defined:

  • tasks_waiting: The number of tasks blocked on this lot’s park() method.

unpark(*, count=1)

Unpark one or more tasks.

This wakes up count tasks that are blocked in park(). If there are fewer than count tasks parked, then wakes as many tasks are available and then returns successfully.

Parameters

count (int) – the number of tasks to unpark.

unpark_all()

Unpark all parked tasks.

Low-level checkpoint functions

await trio.lowlevel.checkpoint()

A pure checkpoint.

This checks for cancellation and allows other tasks to be scheduled, without otherwise blocking.

Note that the scheduler has the option of ignoring this and continuing to run the current task if it decides this is appropriate (e.g. for increased efficiency).

Equivalent to await trio.sleep(0) (which is implemented by calling checkpoint().)

The next two functions are used together to make up a checkpoint:

await trio.lowlevel.checkpoint_if_cancelled()

Issue a checkpoint if the calling context has been cancelled.

Equivalent to (but potentially more efficient than):

if trio.current_effective_deadline() == -inf:
    await trio.lowlevel.checkpoint()

This is either a no-op, or else it allow other tasks to be scheduled and then raises trio.Cancelled.

Typically used together with cancel_shielded_checkpoint().

await trio.lowlevel.cancel_shielded_checkpoint()

Introduce a schedule point, but not a cancel point.

This is not a checkpoint, but it is half of a checkpoint, and when combined with checkpoint_if_cancelled() it can make a full checkpoint.

Equivalent to (but potentially more efficient than):

with trio.CancelScope(shield=True):
    await trio.lowlevel.checkpoint()

These are commonly used in cases where you have an operation that might-or-might-not block, and you want to implement Trio’s standard checkpoint semantics. Example:

async def operation_that_maybe_blocks():
    await checkpoint_if_cancelled()
    try:
        ret = attempt_operation()
    except BlockingIOError:
        # need to block and then retry, which we do below
        pass
    else:
        # operation succeeded, finish the checkpoint then return
        await cancel_shielded_checkpoint()
        return ret
    while True:
        await wait_for_operation_to_be_ready()
        try:
            return attempt_operation()
        except BlockingIOError:
            pass

This logic is a bit convoluted, but accomplishes all of the following:

  • Every successful execution path passes through a checkpoint (assuming that wait_for_operation_to_be_ready is an unconditional checkpoint)

  • Our cancellation semantics say that Cancelled should only be raised if the operation didn’t happen. Using cancel_shielded_checkpoint() on the early-exit branch accomplishes this.

  • On the path where we do end up blocking, we don’t pass through any schedule points before that, which avoids some unnecessary work.

  • Avoids implicitly chaining the BlockingIOError with any errors raised by attempt_operation or wait_for_operation_to_be_ready, by keeping the while True: loop outside of the except BlockingIOError: block.

These functions can also be useful in other situations. For example, when trio.to_thread.run_sync() schedules some work to run in a worker thread, it blocks until the work is finished (so it’s a schedule point), but by default it doesn’t allow cancellation. So to make sure that the call always acts as a checkpoint, it calls checkpoint_if_cancelled() before starting the thread.

Low-level blocking

await trio.lowlevel.wait_task_rescheduled(abort_func)

Put the current task to sleep, with cancellation support.

This is the lowest-level API for blocking in Trio. Every time a Task blocks, it does so by calling this function (usually indirectly via some higher-level API).

This is a tricky interface with no guard rails. If you can use ParkingLot or the built-in I/O wait functions instead, then you should.

Generally the way it works is that before calling this function, you make arrangements for “someone” to call reschedule() on the current task at some later point.

Then you call wait_task_rescheduled(), passing in abort_func, an “abort callback”.

(Terminology: in Trio, “aborting” is the process of attempting to interrupt a blocked task to deliver a cancellation.)

There are two possibilities for what happens next:

  1. “Someone” calls reschedule() on the current task, and wait_task_rescheduled() returns or raises whatever value or error was passed to reschedule().

  2. The call’s context transitions to a cancelled state (e.g. due to a timeout expiring). When this happens, the abort_func is called. Its interface looks like:

    def abort_func(raise_cancel):
        ...
        return trio.lowlevel.Abort.SUCCEEDED  # or FAILED
    

    It should attempt to clean up any state associated with this call, and in particular, arrange that reschedule() will not be called later. If (and only if!) it is successful, then it should return Abort.SUCCEEDED, in which case the task will automatically be rescheduled with an appropriate Cancelled error.

    Otherwise, it should return Abort.FAILED. This means that the task can’t be cancelled at this time, and still has to make sure that “someone” eventually calls reschedule().

    At that point there are again two possibilities. You can simply ignore the cancellation altogether: wait for the operation to complete and then reschedule and continue as normal. (For example, this is what trio.to_thread.run_sync() does if cancellation is disabled.) The other possibility is that the abort_func does succeed in cancelling the operation, but for some reason isn’t able to report that right away. (Example: on Windows, it’s possible to request that an async (“overlapped”) I/O operation be cancelled, but this request is also asynchronous – you don’t find out until later whether the operation was actually cancelled or not.) To report a delayed cancellation, then you should reschedule the task yourself, and call the raise_cancel callback passed to abort_func to raise a Cancelled (or possibly KeyboardInterrupt) exception into this task. Either of the approaches sketched below can work:

    # Option 1:
    # Catch the exception from raise_cancel and inject it into the task.
    # (This is what Trio does automatically for you if you return
    # Abort.SUCCEEDED.)
    trio.lowlevel.reschedule(task, outcome.capture(raise_cancel))
    
    # Option 2:
    # wait to be woken by "someone", and then decide whether to raise
    # the error from inside the task.
    outer_raise_cancel = None
    def abort(inner_raise_cancel):
        nonlocal outer_raise_cancel
        outer_raise_cancel = inner_raise_cancel
        TRY_TO_CANCEL_OPERATION()
        return trio.lowlevel.Abort.FAILED
    await wait_task_rescheduled(abort)
    if OPERATION_WAS_SUCCESSFULLY_CANCELLED:
        # raises the error
        outer_raise_cancel()
    

    In any case it’s guaranteed that we only call the abort_func at most once per call to wait_task_rescheduled().

Sometimes, it’s useful to be able to share some mutable sleep-related data between the sleeping task, the abort function, and the waking task. You can use the sleeping task’s custom_sleep_data attribute to store this data, and Trio won’t touch it, except to make sure that it gets cleared when the task is rescheduled.

Warning

If your abort_func raises an error, or returns any value other than Abort.SUCCEEDED or Abort.FAILED, then Trio will crash violently. Be careful! Similarly, it is entirely possible to deadlock a Trio program by failing to reschedule a blocked task, or cause havoc by calling reschedule() too many times. Remember what we said up above about how you should use a higher-level API if at all possible?

class trio.lowlevel.Abort(value)

enum.Enum used as the return value from abort functions.

See wait_task_rescheduled() for details.

SUCCEEDED
FAILED
trio.lowlevel.reschedule(task, next_send=<object object>)

Reschedule the given task with the given outcome.Outcome.

See wait_task_rescheduled() for the gory details.

There must be exactly one call to reschedule() for every call to wait_task_rescheduled(). (And when counting, keep in mind that returning Abort.SUCCEEDED from an abort callback is equivalent to calling reschedule() once.)

Parameters

Here’s an example lock class implemented using wait_task_rescheduled() directly. This implementation has a number of flaws, including lack of fairness, O(n) cancellation, missing error checking, failure to insert a checkpoint on the non-blocking path, etc. If you really want to implement your own lock, then you should study the implementation of trio.Lock and use ParkingLot, which handles some of these issues for you. But this does serve to illustrate the basic structure of the wait_task_rescheduled() API:

class NotVeryGoodLock:
    def __init__(self):
        self._blocked_tasks = collections.deque()
        self._held = False

    async def acquire(self):
        while self._held:
            task = trio.lowlevel.current_task()
            self._blocked_tasks.append(task)
            def abort_fn(_):
                self._blocked_tasks.remove(task)
                return trio.lowlevel.Abort.SUCCEEDED
            await trio.lowlevel.wait_task_rescheduled(abort_fn)
        self._held = True

    def release(self):
        self._held = False
        if self._blocked_tasks:
            woken_task = self._blocked_tasks.popleft()
            trio.lowlevel.reschedule(woken_task)

Task API

trio.lowlevel.current_root_task()

Returns the current root Task.

This is the task that is the ultimate parent of all other tasks.

trio.lowlevel.current_task()

Return the Task object representing the current task.

Returns

the Task that called current_task().

Return type

Task

class trio.lowlevel.Task

A Task object represents a concurrent “thread” of execution. It has no public constructor; Trio internally creates a Task object for each call to nursery.start(...) or nursery.start_soon(...).

Its public members are mostly useful for introspection and debugging:

name

String containing this Task's name. Usually the name of the function this Task is running, but can be overridden by passing name= to start or start_soon.

coro

This task’s coroutine object. Example usage: extracting a stack trace:

import traceback

def walk_coro_stack(coro):
    while coro is not None:
        if hasattr(coro, "cr_frame"):
            # A real coroutine
            yield coro.cr_frame, coro.cr_frame.f_lineno
            coro = coro.cr_await
        else:
            # A generator decorated with @types.coroutine
            yield coro.gi_frame, coro.gi_frame.f_lineno
            coro = coro.gi_yieldfrom

def print_stack_for_task(task):
    ss = traceback.StackSummary.extract(walk_coro_stack(task.coro))
    print("".join(ss.format()))
context

This task’s contextvars.Context object.

parent_nursery

The nursery this task is inside (or None if this is the “init” task).

Example use case: drawing a visualization of the task tree in a debugger.

eventual_parent_nursery

The nursery this task will be inside after it calls task_status.started().

If this task has already called started(), or if it was not spawned using nursery.start(), then its eventual_parent_nursery is None.

child_nurseries

The nurseries this task contains.

This is a list, with outer nurseries before inner nurseries.

custom_sleep_data

Trio doesn’t assign this variable any meaning, except that it sets it to None whenever a task is rescheduled. It can be used to share data between the different tasks involved in putting a task to sleep and then waking it up again. (See wait_task_rescheduled() for details.)

Using “guest mode” to run Trio on top of other event loops

What is “guest mode”?

An event loop acts as a central coordinator to manage all the IO happening in your program. Normally, that means that your application has to pick one event loop, and use it for everything. But what if you like Trio, but also need to use a framework like Qt or PyGame that has its own event loop? Then you need some way to run both event loops at once.

It is possible to combine event loops, but the standard approaches all have significant downsides:

  • Polling: this is where you use a busy-loop to manually check for IO on both event loops many times per second. This adds latency, and wastes CPU time and electricity.

  • Pluggable IO backends: this is where you reimplement one of the event loop APIs on top of the other, so you effectively end up with just one event loop. This requires a significant amount of work for each pair of event loops you want to integrate, and different backends inevitably end up with inconsistent behavior, forcing users to program against the least-common-denominator. And if the two event loops expose different feature sets, it may not even be possible to implement one in terms of the other.

  • Running the two event loops in separate threads: This works, but most event loop APIs aren’t thread-safe, so in this approach you need to keep careful track of which code runs on which event loop, and remember to use explicit inter-thread messaging whenever you interact with the other loop – or else risk obscure race conditions and data corruption.

That’s why Trio offers a fourth option: guest mode. Guest mode lets you execute trio.run on top of some other “host” event loop, like Qt. Its advantages are:

  • Efficiency: guest mode is event-driven instead of using a busy-loop, so it has low latency and doesn’t waste electricity.

  • No need to think about threads: your Trio code runs in the same thread as the host event loop, so you can freely call sync Trio APIs from the host, and call sync host APIs from Trio. For example, if you’re making a GUI app with Qt as the host loop, then making a cancel button and connecting it to a trio.CancelScope is as easy as writing:

    # Trio code can create Qt objects without any special ceremony...
    my_cancel_button = QPushButton("Cancel")
    # ...and Qt can call back to Trio just as easily
    my_cancel_button.clicked.connect(my_cancel_scope.cancel)
    

    (For async APIs, it’s not that simple, but you can use sync APIs to build explicit bridges between the two worlds, e.g. by passing async functions and their results back and forth through queues.)

  • Consistent behavior: guest mode uses the same code as regular Trio: the same scheduler, same IO code, same everything. So you get the full feature set and everything acts the way you expect.

  • Simple integration and broad compatibility: pretty much every event loop offers some threadsafe “schedule a callback” operation, and that’s all you need to use it as a host loop.

Really? How is that possible?

Note

You can use guest mode without reading this section. It’s included for those who enjoy understanding how things work.

All event loops have the same basic structure. They loop through two operations, over and over:

  1. Wait for the operating system to notify them that something interesting has happened, like data arriving on a socket or a timeout passing. They do this by invoking a platform-specific sleep_until_something_happens() system call – select, epoll, kqueue, GetQueuedCompletionEvents, etc.

  2. Run all the user tasks that care about whatever happened, then go back to step 1.

The problem here is step 1. Two different event loops on the same thread can take turns running user tasks in step 2, but when they’re idle and nothing is happening, they can’t both invoke their own sleep_until_something_happens() function at the same time.

The “polling” and “pluggable backend” strategies solve this by hacking the loops so both step 1s can run at the same time in the same thread. Keeping everything in one thread is great for step 2, but the step 1 hacks create problems.

The “separate threads” strategy solves this by moving both steps into separate threads. This makes step 1 work, but the downside is that now the user tasks in step 2 are running separate threads as well, so users are forced to deal with inter-thread coordination.

The idea behind guest mode is to combine the best parts of each approach: we move Trio’s step 1 into a separate worker thread, while keeping Trio’s step 2 in the main host thread. This way, when the application is idle, both event loops do their sleep_until_something_happens() at the same time in their own threads. But when the app wakes up and your code is actually running, it all happens in a single thread. The threading trickiness is all handled transparently inside Trio.

Concretely, we unroll Trio’s internal event loop into a chain of callbacks, and as each callback finishes, it schedules the next callback onto the host loop or a worker thread as appropriate. So the only thing the host loop has to provide is a way to schedule a callback onto the main thread from a worker thread.

Coordinating between Trio and the host loop does add some overhead. The main cost is switching in and out of the background thread, since this requires cross-thread messaging. This is cheap (on the order of a few microseconds, assuming your host loop is implemented efficiently), but it’s not free.

But, there’s a nice optimization we can make: we only need the thread when our sleep_until_something_happens() call actually sleeps, that is, when the Trio part of your program is idle and has nothing to do. So before we switch into the worker thread, we double-check whether we’re idle, and if not, then we skip the worker thread and jump directly to step 2. This means that your app only pays the extra thread-switching penalty at moments when it would otherwise be sleeping, so it should have minimal effect on your app’s overall performance.

The total overhead will depend on your host loop, your platform, your application, etc. But we expect that in most cases, apps running in guest mode should only be 5-10% slower than the same code using trio.run. If you find that’s not true for your app, then please let us know and we’ll see if we can fix it!

Implementing guest mode for your favorite event loop

Let’s walk through what you need to do to integrate Trio’s guest mode with your favorite event loop. Treat this section like a checklist.

Getting started: The first step is to get something basic working. Here’s a minimal example of running Trio on top of asyncio, that you can use as a model:

import asyncio, trio

# A tiny Trio program
async def trio_main():
    for _ in range(5):
        print("Hello from Trio!")
        # This is inside Trio, so we have to use Trio APIs
        await trio.sleep(1)
    return "trio done!"

# The code to run it as a guest inside asyncio
async def asyncio_main():
    asyncio_loop = asyncio.get_running_loop()

    def run_sync_soon_threadsafe(fn):
        asyncio_loop.call_soon_threadsafe(fn)

    def done_callback(trio_main_outcome):
        print(f"Trio program ended with: {trio_main_outcome}")

    # This is where the magic happens:
    trio.lowlevel.start_guest_run(
        trio_main,
        run_sync_soon_threadsafe=run_sync_soon_threadsafe,
        done_callback=done_callback,
    )

    # Let the host loop run for a while to give trio_main time to
    # finish. (WARNING: This is a hack. See below for better
    # approaches.)
    #
    # This function is in asyncio, so we have to use asyncio APIs.
    await asyncio.sleep(10)

asyncio.run(asyncio_main())

You can see we’re using asyncio-specific APIs to start up a loop, and then we call trio.lowlevel.start_guest_run. This function is very similar to trio.run, and takes all the same arguments. But it has two differences:

First, instead of blocking until trio_main has finished, it schedules trio_main to start running on top of the host loop, and then returns immediately. So trio_main is running in the background – that’s why we have to sleep and give it time to finish.

And second, it requires two extra keyword arguments: run_sync_soon_threadsafe, and done_callback.

For run_sync_soon_threadsafe, we need a function that takes a synchronous callback, and schedules it to run on your host loop. And this function needs to be “threadsafe” in the sense that you can safely call it from any thread. So you need to figure out how to write a function that does that using your host loop’s API. For asyncio, this is easy because call_soon_threadsafe does exactly what we need; for your loop, it might be more or less complicated.

For done_callback, you pass in a function that Trio will automatically invoke when the Trio run finishes, so you know it’s done and what happened. For this basic starting version, we just print the result; in the next section we’ll discuss better alternatives.

At this stage you should be able to run a simple Trio program inside your host loop. Now we’ll turn that prototype into something solid.

Loop lifetimes: One of the trickiest things in most event loops is shutting down correctly. And having two event loops makes this even harder!

If you can, we recommend following this pattern:

  • Start up your host loop

  • Immediately call start_guest_run to start Trio

  • When Trio finishes and your done_callback is invoked, shut down the host loop

  • Make sure that nothing else shuts down your host loop

This way, your two event loops have the same lifetime, and your program automatically exits when your Trio function finishes.

Here’s how we’d extend our asyncio example to implement this pattern:

# Improved version, that shuts down properly after Trio finishes
async def asyncio_main():
    asyncio_loop = asyncio.get_running_loop()

    def run_sync_soon_threadsafe(fn):
        asyncio_loop.call_soon_threadsafe(fn)

    # Revised 'done' callback: set a Future
    done_fut = asyncio_loop.create_future()
    def done_callback(trio_main_outcome):
        done_fut.set_result(trio_main_outcome)

    trio.lowlevel.start_guest_run(
        trio_main,
        run_sync_soon_threadsafe=run_sync_soon_threadsafe,
        done_callback=done_callback,
    )

    # Wait for the guest run to finish
    trio_main_outcome = await done_fut
    # Pass through the return value or exception from the guest run
    return trio_main_outcome.unwrap()

And then you can encapsulate all this machinery in a utility function that exposes a trio.run-like API, but runs both loops together:

def trio_run_with_asyncio(trio_main, *args, **trio_run_kwargs):
    async def asyncio_main():
        # same as above
        ...

    return asyncio.run(asyncio_main())

Technically, it is possible to use other patterns. But there are some important limitations you have to respect:

  • You must let the Trio program run to completion. Many event loops let you stop the event loop at any point, and any pending callbacks/tasks/etc. just… don’t run. Trio follows a more structured system, where you can cancel things, but the code always runs to completion, so finally blocks run, resources are cleaned up, etc. If you stop your host loop early, before the done_callback is invoked, then that cuts off the Trio run in the middle without a chance to clean up. This can leave your code in an inconsistent state, and will definitely leave Trio’s internals in an inconsistent state, which will cause errors if you try to use Trio again in that thread.

    Some programs need to be able to quit at any time, for example in response to a GUI window being closed or a user selecting a “Quit” from a menu. In these cases, we recommend wrapping your whole program in a trio.CancelScope, and cancelling it when you want to quit.

  • Each host loop can only have one start_guest_run at a time. If you try to start a second one, you’ll get an error. If you need to run multiple Trio functions at the same time, then start up a single Trio run, open a nursery, and then start your functions as child tasks in that nursery.

  • Unless you or your host loop register a handler for signal.SIGINT before starting Trio (this is not common), then Trio will take over delivery of KeyboardInterrupts. And since Trio can’t tell which host code is safe to interrupt, it will only deliver KeyboardInterrupt into the Trio part of your code. This is fine if your program is set up to exit when the Trio part exits, because the KeyboardInterrupt will propagate out of Trio and then trigger the shutdown of your host loop, which is just what you want.

Given these constraints, we think the simplest approach is to always start and stop the two loops together.

Signal management: “Signals” are a low-level inter-process communication primitive. When you hit control-C to kill a program, that uses a signal. Signal handling in Python has a lot of moving parts. One of those parts is signal.set_wakeup_fd, which event loops use to make sure that they wake up when a signal arrives so they can respond to it. (If you’ve ever had an event loop ignore you when you hit control-C, it was probably because they weren’t using signal.set_wakeup_fd correctly.)

But, only one event loop can use signal.set_wakeup_fd at a time. And in guest mode that can cause problems: Trio and the host loop might start fighting over who’s using signal.set_wakeup_fd.

Some event loops, like asyncio, won’t work correctly unless they win this fight. Fortunately, Trio is a little less picky: as long as someone makes sure that the program wakes up when a signal arrives, it should work correctly. So if your host loop wants signal.set_wakeup_fd, then you should disable Trio’s signal.set_wakeup_fd support, and then both loops will work correctly.

On the other hand, if your host loop doesn’t use signal.set_wakeup_fd, then the only way to make everything work correctly is to enable Trio’s signal.set_wakeup_fd support.

By default, Trio assumes that your host loop doesn’t use signal.set_wakeup_fd. It does try to detect when this creates a conflict with the host loop, and print a warning – but unfortunately, by the time it detects it, the damage has already been done. So if you’re getting this warning, then you should disable Trio’s signal.set_wakeup_fd support by passing host_uses_signal_set_wakeup_fd=True to start_guest_run.

If you aren’t seeing any warnings with your initial prototype, you’re probably fine. But the only way to be certain is to check your host loop’s source. For example, asyncio may or may not use signal.set_wakeup_fd depending on the Python version and operating system.

A small optimization: Finally, consider a small optimization. Some event loops offer two versions of their “call this function soon” API: one that can be used from any thread, and one that can only be used from the event loop thread, with the latter being cheaper. For example, asyncio has both call_soon_threadsafe and call_soon.

If you have a loop like this, then you can also pass a run_sync_soon_not_threadsafe=... kwarg to start_guest_run, and Trio will automatically use it when appropriate.

If your loop doesn’t have a split like this, then don’t worry about it; run_sync_soon_not_threadsafe= is optional. (If it’s not passed, then Trio will just use your threadsafe version in all cases.)

That’s it! If you’ve followed all these steps, you should now have a cleanly-integrated hybrid event loop. Go make some cool GUIs/games/whatever!

Limitations

In general, almost all Trio features should work in guest mode. The exception is features which rely on Trio having a complete picture of everything that your program is doing, since obviously, it can’t control the host loop or see what it’s doing.

Custom clocks can be used in guest mode, but they only affect Trio timeouts, not host loop timeouts. And the autojump clock and related trio.testing.wait_all_tasks_blocked can technically be used in guest mode, but they’ll only take Trio tasks into account when decided whether to jump the clock or whether all tasks are blocked.

Reference

trio.lowlevel.start_guest_run(async_fn, *args, run_sync_soon_threadsafe, done_callback, run_sync_soon_not_threadsafe=None, host_uses_signal_set_wakeup_fd=False, clock=None, instruments=(), restrict_keyboard_interrupt_to_checkpoints=False)

Start a “guest” run of Trio on top of some other “host” event loop.

Each host loop can only have one guest run at a time.

You should always let the Trio run finish before stopping the host loop; if not, it may leave Trio’s internal data structures in an inconsistent state. You might be able to get away with it if you immediately exit the program, but it’s safest not to go there in the first place.

Generally, the best way to do this is wrap this in a function that starts the host loop and then immediately starts the guest run, and then shuts down the host when the guest run completes.

Parameters
  • run_sync_soon_threadsafe

    An arbitrary callable, which will be passed a function as its sole argument:

    def my_run_sync_soon_threadsafe(fn):
        ...
    

    This callable should schedule fn() to be run by the host on its next pass through its loop. Must support being called from arbitrary threads.

  • done_callback

    An arbitrary callable:

    def my_done_callback(run_outcome):
        ...
    

    When the Trio run has finished, Trio will invoke this callback to let you know. The argument is an outcome.Outcome, reporting what would have been returned or raised by trio.run. This function can do anything you want, but commonly you’ll want it to shut down the host loop, unwrap the outcome, etc.

  • run_sync_soon_not_threadsafe – Like run_sync_soon_threadsafe, but will only be called from inside the host loop’s main thread. Optional, but if your host loop allows you to implement this more efficiently than run_sync_soon_threadsafe then passing it will make things a bit faster.

  • host_uses_signal_set_wakeup_fd (bool) – Pass True if your host loop uses signal.set_wakeup_fd, and False otherwise. For more details, see Implementing guest mode for your favorite event loop.

For the meaning of other arguments, see trio.run.

Handing off live coroutine objects between coroutine runners

Internally, Python’s async/await syntax is built around the idea of “coroutine objects” and “coroutine runners”. A coroutine object represents the state of an async callstack. But by itself, this is just a static object that sits there. If you want it to do anything, you need a coroutine runner to push it forward. Every Trio task has an associated coroutine object (see Task.coro), and the Trio scheduler acts as their coroutine runner.

But of course, Trio isn’t the only coroutine runner in Python – asyncio has one, other event loops have them, you can even define your own.

And in some very, very unusual circumstances, it even makes sense to transfer a single coroutine object back and forth between different coroutine runners. That’s what this section is about. This is an extremely exotic use case, and assumes a lot of expertise in how Python async/await works internally. For motivating examples, see trio-asyncio issue #42, and trio issue #649. For more details on how coroutines work, we recommend André Caron’s A tale of event loops, or going straight to PEP 492 for the full details.

await trio.lowlevel.permanently_detach_coroutine_object(final_outcome)

Permanently detach the current task from the Trio scheduler.

Normally, a Trio task doesn’t exit until its coroutine object exits. When you call this function, Trio acts like the coroutine object just exited and the task terminates with the given outcome. This is useful if you want to permanently switch the coroutine object over to a different coroutine runner.

When the calling coroutine enters this function it’s running under Trio, and when the function returns it’s running under the foreign coroutine runner.

You should make sure that the coroutine object has released any Trio-specific resources it has acquired (e.g. nurseries).

Parameters

final_outcome (outcome.Outcome) – Trio acts as if the current task exited with the given return value or exception.

Returns or raises whatever value or exception the new coroutine runner uses to resume the coroutine.

await trio.lowlevel.temporarily_detach_coroutine_object(abort_func)

Temporarily detach the current coroutine object from the Trio scheduler.

When the calling coroutine enters this function it’s running under Trio, and when the function returns it’s running under the foreign coroutine runner.

The Trio Task will continue to exist, but will be suspended until you use reattach_detached_coroutine_object() to resume it. In the mean time, you can use another coroutine runner to schedule the coroutine object. In fact, you have to – the function doesn’t return until the coroutine is advanced from outside.

Note that you’ll need to save the current Task object to later resume; you can retrieve it with current_task(). You can also use this Task object to retrieve the coroutine object – see Task.coro.

Parameters

abort_func – Same as for wait_task_rescheduled(), except that it must return Abort.FAILED. (If it returned Abort.SUCCEEDED, then Trio would attempt to reschedule the detached task directly without going through reattach_detached_coroutine_object(), which would be bad.) Your abort_func should still arrange for whatever the coroutine object is doing to be cancelled, and then reattach to Trio and call the raise_cancel callback, if possible.

Returns or raises whatever value or exception the new coroutine runner uses to resume the coroutine.

await trio.lowlevel.reattach_detached_coroutine_object(task, yield_value)

Reattach a coroutine object that was detached using temporarily_detach_coroutine_object().

When the calling coroutine enters this function it’s running under the foreign coroutine runner, and when the function returns it’s running under Trio.

This must be called from inside the coroutine being resumed, and yields whatever value you pass in. (Presumably you’ll pass a value that will cause the current coroutine runner to stop scheduling this task.) Then the coroutine is resumed by the Trio scheduler at the next opportunity.

Parameters
  • task (Task) – The Trio task object that the current coroutine was detached from.

  • yield_value (object) – The object to yield to the current coroutine runner.

Design and internals

Here we’ll discuss Trio’s overall design and architecture: how it fits together and why we made the decisions we did. If all you want to do is use Trio, then you don’t need to read this – though you might find it interesting. The main target audience here is (a) folks who want to read the code and potentially contribute, (b) anyone working on similar libraries who want to understand what we’re up to, (c) anyone interested in I/O library design generally.

There are many valid approaches to writing an async I/O library. This is ours.

High-level design principles

Trio’s two overriding goals are usability and correctness: we want to make it easy to get things right.

Of course there are lots of other things that matter too, like speed, maintainability, etc. We want those too, as much as we can get. But sometimes these things come in conflict, and when that happens, these are our priorities.

In some sense the entire rest of this document is a description of how these play out, but to give a simple example: Trio’s KeyboardInterrupt handling machinery is a bit tricky and hard to test, so it scores poorly on simplicity and maintainability. But we think the usability+correctness gains outweigh this.

There are some subtleties here. Notice that it’s specifically “easy to get things right”. There are situations (e.g. writing one-off scripts) where the most “usable” tool is the one that will happily ignore errors and keep going no matter what, or that doesn’t bother with resource cleanup. (Cf. the success of PHP.) This is a totally valid use case and valid definition of usability, but it’s not the one we use: we think it’s easier to build reliable and correct systems if exceptions propagate until handled and if the system catches you when you make potentially dangerous resource handling errors, so that’s what we optimize for.

It’s also worth saying something about speed, since it often looms large in comparisons between I/O libraries. This is a rather subtle and complex topic.

In general, speed is certainly important – but the fact that people sometimes use Python instead of C is a pretty good indicator that usability often trumps speed in practice. We want to make Trio fast, but it’s not an accident that it’s left off our list of overriding goals at the top: if necessary we are willing to accept some slowdowns in the service of usability and reliability.

To break things down in more detail:

First of all, there are the cases where speed directly impacts correctness, like when you hit an accidental O(N**2) algorithm and your program effectively locks up. Trio is very careful to use algorithms and data structures that have good worst-case behavior (even if this might mean sacrificing a few percentage points of speed in the average case).

Similarly, when there’s a conflict, we care more about 99th percentile latencies than we do about raw throughput, because insufficient throughput – if it’s consistent! – can often be budgeted for and handled with horizontal scaling, but once you lose latency it’s gone forever, and latency spikes can easily cross over to become a correctness issue (e.g., an RPC server that responds slowly enough to trigger timeouts is effectively non-functional). Again, of course, this doesn’t mean we don’t care about throughput – but sometimes engineering requires making trade-offs, especially for early-stage projects that haven’t had time to optimize for all use cases yet.

And finally: we care about speed on real-world applications quite a bit, but speed on microbenchmarks is just about our lowest priority. We aren’t interested in competing to build “the fastest echo server in the West”. I mean, it’s nice if it happens or whatever, and microbenchmarks are an invaluable tool for understanding a system’s behavior. But if you play that game to win then it’s very easy to get yourself into a situation with seriously misaligned incentives, where you have to start compromising on features and correctness in order to get a speedup that’s totally irrelevant to real-world applications. In most cases (we suspect) it’s the application code that’s the bottleneck, and you’ll get more of a win out of running the whole app under PyPy than out of any heroic optimizations to the I/O layer. (And this is why Trio does place a priority on PyPy compatibility.)

As a matter of tactics, we also note that at this stage in Trio’s lifecycle, it’d probably be a mistake to worry about speed too much. It doesn’t make sense to spend lots of effort optimizing an API whose semantics are still in flux.

User-level API principles

Basic principles

Trio is very much a continuation of the ideas explored in this blog post, and in particular the principles identified there that make curio easier to use correctly than asyncio. So Trio also adopts these rules, in particular:

  • The only form of concurrency is the task.

  • Tasks are guaranteed to run to completion.

  • Task spawning is always explicit. No callbacks, no implicit concurrency, no futures/deferreds/promises/other APIs that involve callbacks. All APIs are “causal” except for those that are explicitly used for task spawning.

  • Exceptions are used for error handling; try/finally and with blocks for handling cleanup.

Cancel points and schedule points

The first major place that Trio departs from curio is in its decision to make a much larger fraction of the API use sync functions rather than async functions, and to provide strong conventions about cancel points and schedule points. (At this point, there are a lot of ways that Trio and curio have diverged. But this was really the origin – the tipping point where I realized that exploring these ideas would require a new library, and couldn’t be done inside curio.) The full reasoning here takes some unpacking.

First, some definitions: a cancel point is a point where your code checks if it has been cancelled – e.g., due to a timeout having expired – and potentially raises a Cancelled error. A schedule point is a point where the current task can potentially be suspended, and another task allowed to run.

In curio, the convention is that all operations that interact with the run loop in any way are syntactically async, and it’s undefined which of these operations are cancel/schedule points; users are instructed to assume that any of them might be cancel/schedule points, but with a few exceptions there’s no guarantee that any of them are unless they actually block. (I.e., whether a given call acts as a cancel/schedule point is allowed to vary across curio versions and also depending on runtime factors like network load.)

But when using an async library, there are good reasons why you need to be aware of cancel and schedule points. They introduce a set of complex and partially conflicting constraints on your code:

You need to make sure that every task passes through a cancel point regularly, because otherwise timeouts become ineffective and your code becomes subject to DoS attacks and other problems. So for correctness, it’s important to make sure you have enough cancel points.

But… every cancel point also increases the chance of subtle bugs in your program, because it’s a place where you have to be prepared to handle a Cancelled exception and clean up properly. And while we try to make this as easy as possible, these kinds of clean-up paths are notorious for getting missed in testing and harboring subtle bugs. So the more cancel points you have, the harder it is to make sure your code is correct.

Similarly, you need to make sure that every task passes through a schedule point regularly, because otherwise this task could end up hogging the event loop and preventing other code from running, causing a latency spike. So for correctness, it’s important to make sure you have enough schedule points.

But… you have to be careful here too, because every schedule point is a point where arbitrary other code could run, and alter your program’s state out from under you, introducing classic concurrency bugs. So as you add more schedule points, it becomes exponentially harder to reason about how your code is interleaved and be sure that it’s correct.

So an important question for an async I/O library is: how do we help the user manage these trade-offs?

Trio’s answer is informed by two further observations:

First, any time a task blocks (e.g., because it does an await sock.recv() but there’s no data available to receive), that has to be a cancel point (because if the I/O never arrives, we need to be able to time out), and it has to be a schedule point (because the whole idea of asynchronous programming is that when one task is waiting we can switch to another task to get something useful done).

And second, a function which sometimes counts as a cancel/schedule point, and sometimes doesn’t, is the worst of both worlds: you have put in the effort to make sure your code handles cancellation or interleaving correctly, but you can’t count on it to help meet latency requirements.

With all that in mind, Trio takes the following approach:

Rule 1: to reduce the number of concepts to keep track of, we collapse cancel points and schedule points together. Every point that is a cancel point is also a schedule point and vice versa. These are distinct concepts both theoretically and in the actual implementation, but we hide that distinction from the user so that there’s only one concept they need to keep track of.

Rule 2: Cancel+schedule points are determined statically. A Trio primitive is either always a cancel+schedule point, or never a cancel+schedule point, regardless of runtime conditions. This is because we want it to be possible to determine whether some code has “enough” cancel/schedule points by reading the source code.

In fact, to make this even simpler, we make it so you don’t even have to look at the function arguments: each function is either a cancel+schedule point on every call or on no calls.

(Pragmatic exception: a Trio primitive is not required to act as a cancel+schedule point when it raises an exception, even if it would act as one in the case of a successful return. See issue 474 for more details; basically, requiring checkpoints on all exception paths added a lot of implementation complexity with negligible user-facing benefit.)

Observation: since blocking is always a cancel+schedule point, rule 2 implies that any function that sometimes blocks is always a cancel+schedule point.

So that gives us a number of cancel+schedule points: all the functions that can block. Are there any others? Trio’s answer is: no. It’s easy to add new points explicitly (throw in a sleep(0) or whatever) but hard to get rid of them when you don’t want them. (And this is a real issue – “too many potential cancel points” is definitely a tension I’ve felt while trying to build things like task supervisors in curio.) And we expect that most Trio programs will execute potentially-blocking operations “often enough” to produce reasonable behavior. So, rule 3: the only cancel+schedule points are the potentially-blocking operations.

And now that we know where our cancel+schedule points are, there’s the question of how to effectively communicate this information to the user. We want some way to mark out a category of functions that might block or trigger a task switch, so that they’re clearly distinguished from functions that don’t do this. Wouldn’t it be nice if there were some Python feature, that naturally divided functions into two categories, and maybe put some sort of special syntactic marking on with the functions that can do weird things like block and task switch…? What a coincidence, that’s exactly how async functions work! Rule 4: in Trio, only the potentially blocking functions are async. So e.g. Event.wait() is async, but Event.set() is sync.

Summing up: out of what’s actually a pretty vast space of design possibilities, we declare by fiat that when it comes to Trio primitives, all of these categories are identical:

  • async functions

  • functions that can, under at least some circumstances, block

  • functions where the caller needs to be prepared to handle potential Cancelled exceptions

  • functions that are guaranteed to notice any pending cancellation

  • functions where you need to be prepared for a potential task switch

  • functions that are guaranteed to take care of switching tasks if appropriate

This requires some non-trivial work internally – it actually takes a fair amount of care to make those 4 cancel/schedule categories line up, and there are some shenanigans required to let sync and async APIs both interact with the run loop on an equal footing. But this is all invisible to the user, we feel that it pays off in terms of usability and correctness.

There is one exception to these rules, for async context managers. Context managers are composed of two operations – enter and exit – and sometimes only one of these is potentially blocking. (Examples: async with lock: can block when entering but never when exiting; async with open_nursery() as ...: can block when exiting but never when entering.) But, Python doesn’t have “half-asynchronous” context managers: either both operations are async-flavored, or neither is. In Trio we take a pragmatic approach: for this kind of async context manager, we enforce the above rules only on the potentially blocking operation, and the other operation is allowed to be syntactically async but semantically synchronous. And async context managers should always document which of their operations are schedule+cancel points.

Exceptions always propagate

Another rule that Trio follows is that exceptions must always propagate. This is like the zen line about “Errors should never pass silently”, except that in every other concurrency library for Python (threads, asyncio, curio, …), it’s fairly common to end up with an undeliverable exception, which just gets printed to stderr and then discarded. While we understand the pragmatic constraints that motivated these libraries to adopt this approach, we feel that there are far too many situations where no human will ever look at stderr and notice the problem, and insist that Trio APIs find a way to propagate exceptions “up the stack” – whatever that might mean.

This is often a challenging rule to follow – for example, the call soon code has to jump through some hoops to make it happen – but its most dramatic influence can seen in Trio’s task-spawning interface, where it motivates the use of “nurseries”:

async def parent():
    async with trio.open_nursery() as nursery:
        nursery.start_soon(child)

(See Tasks let you do multiple things at once for full details.)

If you squint you can see the conceptual influence of Erlang’s “task linking” and “task tree” ideas here, though the details are different.

This design also turns out to enforce a remarkable, unexpected invariant.

In the blog post I called out a nice feature of curio’s spawning API, which is that since spawning is the only way to break causality, and in curio spawn is async, which means that in curio sync functions are guaranteed to be causal. One limitation though is that this invariant is actually not very predictive: in curio there are lots of async functions that could spawn off children and violate causality, but most of them don’t, but there’s no clear marker for the ones that do.

Our API doesn’t quite give that guarantee, but actually a better one. In Trio:

  • Sync functions can’t create nurseries, because nurseries require an async with

  • Any async function can create a nursery and start new tasks… but creating a nursery allows task starting but does not permit causality breaking, because the children have to exit before the function is allowed to return. So we can preserve causality without having to give up concurrency!

  • The only way to violate causality (which is an important feature, just one that needs to be handled carefully) is to explicitly create a nursery object in one task and then pass it into another task. And this provides a very clear and precise signal about where the funny stuff is happening – just watch for the nursery object getting passed around.

Introspection, debugging, testing

Tools for introspection and debugging are critical to achieving usability and correctness in practice, so they should be first-class considerations in Trio.

Similarly, the availability of powerful testing tools has a huge impact on usability and correctness; we consider testing helpers to be very much in scope for the Trio project.

Specific style guidelines

  • As noted above, functions that don’t block should be sync-colored, and functions that might block should be async-colored and unconditionally act as cancel+schedule points.

  • Any function that takes a callable to run should have a signature like:

    def call_the_thing(fn, *args, kwonly1, kwonly2, ...)::
        ...
    

    where fn(*args) is the thing to be called, and kwonly1, kwonly2, … are keyword-only arguments that belong to call_the_thing. This applies even if call_the_thing doesn’t take any arguments of its own, i.e. in this case its signature looks like:

    def call_the_thing(fn, *args)::
        ...
    

    This allows users to skip faffing about with functools.partial() in most cases, while still providing an unambiguous and extensible way to pass arguments to the caller. (Hat-tip to asyncio, who we stole this convention from.)

  • Whenever it makes sense, Trio classes should have a method called statistics() which returns an immutable object with named fields containing internal statistics about the object that are useful for debugging or introspection (examples).

  • Functions or methods whose purpose is to wait for a condition to become true should be called wait_<condition>. This avoids ambiguities like “does await readable() check readability (returning a bool) or wait for readability?”.

    Sometimes this leads to the slightly funny looking await wait_.... Sorry. As far as I can tell all the alternatives are worse, and you get used to the convention pretty quick.

  • If it’s desirable to have both blocking and non-blocking versions of a function, then they look like:

    async def OPERATION(...):
        ...
    
    def OPERATION_nowait(...):
        ...
    

    and the nowait version raises trio.WouldBlock if it would block.

  • …we should, but currently don’t, have a solid convention to distinguish between functions that take an async callable and those that take a sync callable. See issue #68.

A brief tour of Trio’s internals

If you want to understand how Trio is put together internally, then the first thing to know is that there’s a very strict internal layering: the trio._core package is a fully self-contained implementation of the core scheduling/cancellation/IO handling logic, and then the other trio.* modules are implemented in terms of the API it exposes. (If you want to see what this API looks like, then import trio; print(trio._core.__all__)). Everything exported from trio._core is also exported as part of the trio, trio.lowlevel, or trio.testing namespaces. (See their respective __init__.py files for details; there’s a test to enforce this.)

Rationale: currently, Trio is a new project in a novel part of the design space, so we don’t make any stability guarantees. But the goal is to reach the point where we can declare the API stable. It’s unlikely that we’ll be able to quickly explore all possible corners of the design space and cover all possible types of I/O. So instead, our strategy is to make sure that it’s possible for independent packages to add new features on top of Trio. Enforcing the trio vs trio._core split is a way of eating our own dogfood: basic functionality like trio.Lock and trio.socket is actually implemented solely in terms of public APIs. And the hope is that by doing this, we increase the chances that someone who comes up with a better kind of queue or wants to add some new functionality like, say, file system change watching, will be able to do that on top of our public APIs without having to modify Trio internals.

Inside trio._core

There are two notable sub-modules that are largely independent of the rest of Trio, and could (possibly should?) be extracted into their own independent packages:

  • _multierror.py: Implements MultiError and associated infrastructure.

  • _ki.py: Implements the core infrastructure for safe handling of KeyboardInterrupt.

The most important submodule, where everything is integrated, is _run.py. (This is also by far the largest submodule; it’d be nice to factor bits of it out where possible, but it’s tricky because the core functionality genuinely is pretty intertwined.) Notably, this is where cancel scopes, nurseries, and Task are defined; it’s also where the scheduler state and trio.run() live.

The one thing that isn’t in _run.py is I/O handling. This is delegated to an IOManager class, of which there are currently three implementations:

  • EpollIOManager in _io_epoll.py (used on Linux, illumos)

  • KqueueIOManager in _io_kqueue.py (used on macOS, *BSD)

  • WindowsIOManager in _io_windows.py (used on Windows)

The epoll and kqueue backends take advantage of the epoll and kqueue wrappers in the stdlib select module. The windows backend uses CFFI to access to the Win32 API directly (see trio/_core/_windows_cffi.py). In general, we prefer to go directly to the raw OS functionality rather than use selectors, for several reasons:

  • Controlling our own fate: I/O handling is pretty core to what Trio is about, and selectors is (as of 2017-03-01) somewhat buggy (e.g. issue 29256, issue 29255). Which isn’t a big deal on its own, but since selectors is part of the standard library we can’t fix it and ship an updated version; we’re stuck with whatever we get. We want more control over our users’ experience than that.

  • Impedance mismatch: the selectors API isn’t particularly well-fitted to how we want to use it. For example, kqueue natively treats an interest in readability of some fd as a separate thing from an interest in that same fd’s writability, which neatly matches Trio’s model. selectors.KqueueSelector goes to some effort internally to lump together all interests in a single fd, and to use it we’d then we’d have to jump through more hoops to reverse this. Of course, the native epoll API is fd-centric in the same way as the selectors API so we do still have to write code to jump through these hoops, but the point is that the selectors abstractions aren’t providing a lot of extra value.

  • (Most important) Access to raw platform capabilities: selectors is highly inadequate on Windows, and even on Unix-like systems it hides a lot of power (e.g. kqueue can do a lot more than just check fd readability/writability!).

The IOManager layer provides a fairly raw exposure of the capabilities of each system, with public API functions that vary between different backends. (This is somewhat inspired by how os works.) These public APIs are then exported as part of trio.lowlevel, and higher-level APIs like trio.socket abstract over these system-specific APIs to provide a uniform experience.

Currently the choice of backend is made statically at import time, and there is no provision for “pluggable” backends. The intuition here is that we’d rather focus our energy on making one set of solid, official backends that provide a high-quality experience out-of-the-box on all supported systems.

Release history

Trio 0.21.0 (2022-06-07)

Features

Deprecations and Removals

  • Remove support for Python 3.6. (#2210)

Trio 0.20.0 (2022-02-21)

Features

Bugfixes

  • Trio now avoids creating cyclic garbage when a MultiError is generated and filtered, including invisibly within the cancellation system. This means errors raised through nurseries and cancel scopes should result in less GC latency. (#2063)

  • Trio now deterministically cleans up file descriptors that were opened before subprocess creation fails. Previously, they would remain open until the next run of the garbage collector. (#2193)

  • Add compatibility with OpenSSL 3.0 on newer Python and PyPy versions by working around SSLEOFError not being raised properly. (#2203)

  • Fix a bug that could cause Process.wait to hang on Linux systems using pidfds, if another task were to access Process.returncode after the process exited but before wait woke up (#2209)

Trio 0.19.0 (2021-06-15)

Features

  • Trio now supports Python 3.10. (#1921)

  • Use slots for Task which should make them slightly smaller and faster. (#1927)

  • Make Event more lightweight by using less objects (about 2 rather than 5, including a nested ParkingLot and attribute dicts) and simpler structures (set rather than OrderedDict). This may benefit applications that create a large number of event instances, such as with the “replace event object on every set()” idiom. (#1948)

Bugfixes

  • The event loop now holds on to references of coroutine frames for only the minimum necessary period of time. (#1864)

  • The TrioToken class can now be used as a target of a weak reference. (#1924)

Trio 0.18.0 (2021-01-11)

Features

Bugfixes

  • Previously, on Windows, Trio programs using thousands of sockets at the same time could trigger extreme slowdowns in the Windows kernel. Now, Trio works around this issue, so you should be able to use as many sockets as you want. (#1280)

  • trio.from_thread.run() no longer crashes the Trio run if it is executed after the system nursery has been closed but before the run has finished. Calls made at this time will now raise trio.RunFinishedError. This fixes a regression introduced in Trio 0.17.0. The window in question is only one scheduler tick long in most cases, but may be longer if async generators need to be cleaned up. (#1738)

  • Fix a crash in pypy-3.7 (#1765)

  • Trio now avoids creating cyclic garbage as often. This should have a minimal impact on most programs, but can slightly reduce how often the cycle collector GC runs on CPython, which can reduce latency spikes. (#1770)

Deprecations and removals

Trio 0.17.0 (2020-09-15)

Headline features

  • Trio now supports automatic async generator finalization, so more async generators will work even if you don’t wrap them in async with async_generator.aclosing(): blocks. Please see the documentation for important caveats; in particular, yielding within a nursery or cancel scope remains unsupported. (#265)

Features

  • trio.open_tcp_stream has a new local_address= keyword argument that can be used on machines with multiple IP addresses to control which IP is used for the outgoing connection. (#275)

  • If you pass a raw IP address into sendto, it no longer spends any time trying to resolve the hostname. If you’re using UDP, this should substantially reduce your per-packet overhead. (#1595)

  • trio.lowlevel.checkpoint is now much faster. (#1613)

  • We switched to a new, lower-overhead data structure to track upcoming timeouts, which should make your programs faster. (#1629)

Bugfixes

  • On macOS and BSDs, explicitly close our wakeup socketpair when we’re done with it. (#1621)

  • Trio can now be imported when sys.excepthook is a functools.partial instance, which might occur in a pytest-qt test function. (#1630)

  • The thread cache didn’t release its reference to the previous job. (#1638)

  • On Windows, Trio now works around the buggy behavior of certain Layered Service Providers (system components that can intercept network activity) that are built on top of a commercially available library called Komodia Redirector. This benefits users of products such as Astrill VPN and Qustodio parental controls. Previously, Trio would crash on startup when run on a system where such a product was installed. (#1659)

Deprecations and removals

  • Remove wait_socket_*, notify_socket_closing, notify_fd_closing, run_sync_in_worker_thread and current_default_worker_thread_limiter. They were deprecated in 0.12.0. (#1596)

Miscellaneous internal changes

  • When using instruments, you now only “pay for what you use”: if there are no instruments installed that override a particular hook such as before_task_step(), then Trio doesn’t waste any effort on checking its instruments when the event corresponding to that hook occurs. Previously, installing any instrument would incur all the instrumentation overhead, even for hooks no one was interested in. (#1340)

Trio 0.16.0 (2020-06-10)

Headline features

Features

  • To speed up trio.to_thread.run_sync, Trio now caches and re-uses worker threads.

    And in case you have some exotic use case where you need to spawn threads manually, but want to take advantage of Trio’s cache, you can do that using the new trio.lowlevel.start_thread_soon. (#6)

  • Tasks spawned with nursery.start() aren’t treated as direct children of their nursery until they call task_status.started(). This is visible through the task tree introspection attributes such as Task.parent_nursery. Sometimes, though, you want to know where the task is going to wind up, even if it hasn’t finished initializing yet. To support this, we added a new attribute Task.eventual_parent_nursery. For a task spawned with start() that hasn’t yet called started(), this is the nursery that the task was nominally started in, where it will be running once it finishes starting up. In all other cases, it is None. (#1558)

Bugfixes

Deprecations and removals

Miscellaneous internal changes

  • We refactored trio.testing.MockClock so that it no longer needs to run an internal task to manage autojumping. This should be mostly invisible to users, but there is one semantic change: the interaction between trio.testing.wait_all_tasks_blocked and the autojump clock was fixed. Now, the autojump will always wait until after all wait_all_tasks_blocked calls have finished before firing, instead of it depending on which threshold values you passed. (#1587)

Trio 0.15.1 (2020-05-22)

Bugfixes

  • Fix documentation build. (This must be a new release tag to get readthedocs “stable” to include the changes from 0.15.0.)

  • Added a helpful error message if an async function is passed to trio.from_thread.run_sync or a sync function to trio.from_thread.run. (#1244)

Trio 0.15.0 (2020-05-19)

Features

  • Previously, when trio.run_process was cancelled, it always killed the subprocess immediately. Now, on Unix, it first gives the process a chance to clean up by sending SIGTERM, and only escalates to SIGKILL if the process is still running after 5 seconds. But if you prefer the old behavior, or want to adjust the timeout, then don’t worry: you can now pass a custom deliver_cancel= argument to define your own process killing policy. (#1104)

  • It turns out that creating a subprocess can block the parent process for a surprisingly long time. So trio.open_process now uses a worker thread to avoid blocking the event loop. (#1109)

  • We’ve added FreeBSD to the list of platforms we support and test on. (#1118)

  • On Linux kernels v5.3 or newer, trio.Process.wait now uses the pidfd API to track child processes. This shouldn’t have any user-visible change, but it makes working with subprocesses faster and use less memory. (#1241)

  • The trio.Process.returncode attribute is now automatically updated as needed, instead of only when you call poll or wait. Also, repr(process_object) now always contains up-to-date information about the process status. (#1315)

Bugfixes

  • On Ubuntu systems, the system Python includes a custom unhandled-exception hook to perform crash reporting. Unfortunately, Trio wants to use the same hook to print nice MultiError tracebacks, causing a conflict. Previously, Trio would detect the conflict, print a warning, and you just wouldn’t get nice MultiError tracebacks. Now, Trio has gotten clever enough to integrate its hook with Ubuntu’s, so the two systems should Just Work together. (#1065)

  • Fixed an over-strict test that caused failures on Alpine Linux. Started testing against Alpine in CI. (#1499)

  • Calling open_signal_receiver with no arguments used to succeed without listening for any signals. This was confusing, so now it raises TypeError instead. (#1526)

Deprecations and Removals

  • Remove support for Python 3.5. (#75)

  • It turns out that everyone got confused by the name trio.hazmat. So that name has been deprecated, and the new name is trio.lowlevel. (#476)

  • Most of the public classes that Trio exports – like trio.Lock, trio.SocketStream, and so on – weren’t designed with subclassing in mind. And we’ve noticed that some users were trying to subclass them anyway, and ending up with fragile code that we’re likely to accidentally break in the future, or else be stuck unable to make changes for fear of breaking subclasses.

    There are also some classes that were explicitly designed to be subclassed, like the ones in trio.abc. Subclassing these is still supported. However, for all other classes, attempts to subclass will now raise a deprecation warning, and in the future will raise an error.

    If this causes problems for you, feel free to drop by our chat room or file a bug, to discuss alternatives or make a case for why some particular class should be designed to support subclassing. (#1044)

  • If you want to create a trio.Process object, you now have to call trio.open_process; calling trio.Process() directly was deprecated in v0.12.0 and has now been removed. (#1109)

  • Remove clear method on trio.Event: it was deprecated in 0.12.0. (#1498)

Trio 0.14.0 (2020-04-27)

Features

  • If you’re using Trio’s low-level interfaces like trio.hazmat.wait_readable or similar, and then you close a socket or file descriptor, you’re supposed to call trio.hazmat.notify_closing first so Trio can clean up properly. But what if you forget? In the past, Trio would tend to either deadlock or explode spectacularly. Now, it’s much more robust to this situation, and should generally survive. (But note that “survive” is not the same as “give you the results you were expecting”, so you should still call notify_closing when appropriate. This is about harm reduction and making it easier to debug this kind of mistake, not something you should rely on.)

    If you’re using higher-level interfaces outside of the trio.hazmat module, then you don’t need to worry about any of this; those intefaces already take care of calling notify_closing for you. (#1272)

Bugfixes

  • A bug related to the following methods has been introduced in version 0.12.0:

    The iteration of the blocking generators produced by pathlib was performed in the trio thread. With this fix, the previous behavior is restored: the blocking generators are converted into lists in a thread dedicated to blocking IO calls. (#1308)

Deprecations and Removals

  • Deprecate Python 3.5 (#1408)

  • Remove trio.open_cancel_scope which was deprecated in 0.11.0. (#1458)

Trio 0.13.0 (2019-11-02)

Features

  • On Windows, the IOCP subsystem is generally the best way to implement async I/O operations – but it’s historically been weak at providing select-style readiness notifications, like trio.hazmat.wait_readable and wait_writable. We aren’t willing to give those up, so previously Trio’s Windows backend used a hybrid of select + IOCP. This was complex, slow, and had limited scalability.

    Fortunately, we found a way to implement wait_* with IOCP, so Trio’s Windows backend has been completely rewritten, and now uses IOCP exclusively. As a user, the only difference you should notice is that Trio should now be faster on Windows, and can handle many more sockets. This also simplified the code internally, which should allow for more improvements in the future.

    However, this is somewhat experimental, so if you use Windows then please keep an eye out and let us know if you run into any problems! (#52)

  • Use slots for memory channel state and statistics which should make memory channels slightly smaller and faster. (#1195)

Bugfixes

  • OpenSSL has a bug in its handling of TLS 1.3 session tickets that can cause deadlocks or data loss in some rare edge cases. These edge cases most frequently happen during tests. (Upstream bug reports: openssl/openssl#7948, openssl/openssl#7967.) trio.SSLStream now works around this issue, so you don’t have to worry about it. (#819)

  • Trio now uses signal.set_wakeup_fd on all platforms. This is mostly an internal refactoring with no user-visible effect, but in theory it should fix a few extremely-rare race conditions on Unix that could have caused signal delivery to be delayed. (#109)

  • Trio no longer crashes when an async function is implemented in C or Cython and then passed directly to trio.run or nursery.start_soon. (#550, #1191)

  • When a Trio task makes improper use of a non-Trio async library, Trio now causes an exception to be raised within the task at the point of the error, rather than abandoning the task and raising an error in its parent. This improves debuggability and resolves the TrioInternalError that would sometimes result from the old strategy. (#552)

  • In 0.12.0 we deprecated trio.run_sync_in_worker_thread in favor of trio.to_thread.run_sync. But, the deprecation message listed the wrong name for the replacement. The message now gives the correct name. (#810)

  • Fix regression introduced with cancellation changes in 0.12.0, where a trio.CancelScope which isn’t cancelled could catch a propagating trio.Cancelled exception if shielding were changed while the cancellation was propagating. (#1175)

  • Fix a crash that could happen when using MockClock with autojump enabled and a non-zero rate. (#1190)

  • If you nest >1000 cancel scopes within each other, Trio now handles that gracefully instead of crashing with a RecursionError. (#1235)

  • Fixed the hash behavior of trio.Path to match pathlib.Path. Previously trio.Path’s hash was inherited from object instead of from pathlib.PurePath. Thus, hashing two trio.Path's or a trio.Path and a pathlib.Path with the same underlying path would yield different results. (#1259)

Trio 0.12.1 (2019-08-01)

Bugfixes

  • In v0.12.0, we accidentally moved BlockingTrioPortal from trio to trio.hazmat. It’s now been restored to its proper position. (It’s still deprecated though, and will issue a warning if you use it.) (#1167)

Trio 0.12.0 (2019-07-31)

Features

  • If you have a ReceiveStream object, you can now use async for data in stream: ... instead of calling receive_some. Each iteration gives an arbitrary sized chunk of bytes. And the best part is, the loop automatically exits when you reach EOF, so you don’t have to check for it yourself anymore. Relatedly, you no longer need to pick a magic buffer size value before calling receive_some; you can await stream.receive_some() with no arguments, and the stream will automatically pick a reasonable size for you. (#959)

  • Threading interfaces have been reworked: run_sync_in_worker_thread is now trio.to_thread.run_sync, and instead of BlockingTrioPortal, use trio.from_thread.run and trio.from_thread.run_sync. What’s neat about this is that these cooperate, so if you’re in a thread created by to_thread.run_sync, it remembers which Trio created it, and you can call trio.from_thread.* directly without having to pass around a BlockingTrioPortal object everywhere. (#810)

  • We cleaned up the distinction between the “abstract channel interface” and the “memory channel” concrete implementation. trio.abc.SendChannel and trio.abc.ReceiveChannel have been slimmed down, trio.MemorySendChannel and trio.MemoryReceiveChannel are now public types that can be used in type hints, and there’s a new trio.abc.Channel interface for future bidirectional channels. (#719)

  • Add trio.run_process() as a high-level helper for running a process and waiting for it to finish, like the standard subprocess.run() does. (#822)

  • On Linux, when wrapping a bare file descriptor in a Trio socket object, Trio now auto-detects the correct family, type, and protocol. This is useful, for example, when implementing systemd socket activation. (#251)

  • Trio sockets have a new method is_readable that allows you to check whether a socket is readable. This is useful for HTTP/1.1 clients. (#760)

  • We no longer use runtime code generation to dispatch core functions like current_time. Static analysis tools like mypy and pylint should now be able to recognize and analyze all of Trio’s top-level functions (though some class attributes are still dynamic… we’re working on it). (#805)

  • Add trio.hazmat.FdStream for wrapping a Unix file descriptor as a Stream. (#829)

  • Trio now gives a reasonable traceback and error message in most cases when its invariants surrounding cancel scope nesting have been violated. (One common source of such violations is an async generator that yields within a cancel scope.) The previous behavior was an inscrutable chain of TrioInternalErrors. (#882)

  • MultiError now defines its exceptions attribute in __init__() to better support linters and code autocompletion. (#1066)

  • Use __slots__ in more places internally, which should make Trio slightly faster. (#984)

Bugfixes

  • Destructor methods (__del__) are now protected against KeyboardInterrupt. (#676)

  • The trio.Path methods glob() and rglob() now return iterables of trio.Path (not pathlib.Path). (#917)

  • Inspecting the cancel_called attribute of a not-yet-exited cancel scope whose deadline is in the past now always returns True, like you might expect. (Previously it would return False for not-yet-entered cancel scopes, and for active cancel scopes until the first checkpoint after their deadline expiry.) (#958)

  • The trio.Path classmethods, home() and cwd(), are now async functions. Previously, a bug in the forwarding logic meant cwd() was synchronous and home() didn’t work at all. (#960)

  • An exception encapsulated within a MultiError doesn’t need to be hashable anymore.

    Note

    This is only supported if you are running python >= 3.6.4. You can refer to this github PR for details. (#1005)

Improved Documentation

  • To help any user reading through Trio’s function implementations, start using public names (not _core) whenever possible. (#1017)

Deprecations and Removals

Miscellaneous internal changes

  • The plumbing of Trio’s cancellation system has been substantially overhauled to improve performance and ease future planned improvements. Notably, there is no longer any internal concept of a “cancel stack”, and checkpoints now take constant time regardless of the cancel scope nesting depth. (#58)

  • We’ve slightly relaxed our definition of which Trio operations act as checkpoints. A Trio async function that exits by throwing an exception is no longer guaranteed to execute a checkpoint; it might or might not. The rules are unchanged for async functions that don’t exit with an exception, async iterators, and async context managers. trio.testing.assert_checkpoints() has been updated to reflect the new behavior: if its with block exits with an exception, no assertion is made. (#474)

  • Calling str on a trio.Cancelled exception object returns “Cancelled” instead of an empty string. (#674)

  • Change the default timeout in trio.open_tcp_stream() to 0.250 seconds, for consistency with RFC 8305. (#762)

  • On win32 we no longer set SO_EXCLUSIVEADDRUSE when binding a socket in trio.open_tcp_listeners. (#928)

  • Any attempt to inherit from CancelScope or Nursery now raises TypeError. (Trio has never been able to safely support subclassing here; this change just makes it more obvious.) Also exposed as public classes for type-checking, etc. (#1021)

Trio 0.11.0 (2019-02-09)

Features

  • Add support for “unbound cancel scopes”: you can now construct a trio.CancelScope without entering its context, e.g., so you can pass it to another task which will use it to wrap some work that you want to be able to cancel from afar. (#607)

  • The test suite now passes with openssl v1.1.1. Unfortunately this required temporarily disabling TLS v1.3 during tests; see openssl bugs #7948 and #7967. We believe TLS v1.3 should work in most real use cases, but will be monitoring the situation. (#817)

  • Add trio.Process.stdio, which is a StapledStream of stdin and stdout if both of those are available, and None otherwise. This is intended to make it more ergonomic to speak a back-and-forth protocol with a subprocess. (#862)

  • trio.Process on POSIX systems no longer accepts the error-prone combination of shell=False with a command that’s a single string, or shell=True with a command that’s a sequence of strings. These forms are accepted by the underlying subprocess.Popen constructor but don’t do what most users expect. Also, added an explanation of quoting to the documentation. (#863)

  • Added an internal mechanism for pytest-trio’s Hypothesis integration to make the task scheduler reproducible and avoid flaky tests. (#890)

  • SendChannel, ReceiveChannel, Listener, and open_memory_channel() can now be referenced using a generic type parameter (the type of object sent over the channel or produced by the listener) using PEP 484 syntax: trio.abc.SendChannel[bytes], trio.abc.Listener[trio.SocketStream], trio.open_memory_channel[MyMessage](5), etc. The added type information does not change the runtime semantics, but permits better integration with external static type checkers. (#908)

Bugfixes

  • Fixed several bugs in the new Unix subprocess pipe support, where (a) operations on a closed pipe could accidentally affect another unrelated pipe due to internal file-descriptor reuse, (b) in very rare circumstances, two tasks calling send_all on the same pipe at the same time could end up with intermingled data instead of a BusyResourceError. (#661)

  • Stop trio.open_tcp_listeners() from crashing on systems that have disabled IPv6. (#853)

  • Fixed support for multiple tasks calling trio.Process.wait() simultaneously; on kqueue platforms it would previously raise an exception. (#854)

  • trio.Cancelled exceptions now always propagate until they reach the outermost unshielded cancelled scope, even if more cancellations occur or shielding is changed between when the Cancelled is delivered and when it is caught. (#860)

  • If you have a SocketStream that’s already been closed, then await socket_stream.send_all(b"") will now correctly raise ClosedResourceError. (#874)

  • Simplified the Windows subprocess pipe send_all code, and in the process fixed a theoretical bug where closing a pipe at just the wrong time could produce errors or cause data to be redirected to the wrong pipe. (#883)

Deprecations and Removals

  • Deprecate trio.open_cancel_scope in favor of trio.CancelScope, which more clearly reflects that creating a cancel scope is just an ordinary object construction and does not need to be immediately paired with entering it. (#607)

  • The submodules trio.ssl and trio.subprocess are now deprecated. Their nontrivial contents (Process, SSLStream, and SSLListener) have been moved to the main trio namespace. For the numerous constants, exceptions, and other helpers that were previously reexported from the standard ssl and subprocess modules, you should now use those modules directly. (#852)

  • Remove all the APIs deprecated in 0.9.0 or earlier (trio.Queue, trio.catch_signals(), trio.BrokenStreamError, and trio.ResourceBusyError), except for trio.hazmat.UnboundedQueue, which stays for now since it is used by the obscure lowlevel functions monitor_completion_queue() and monitor_kevent(). (#918)

Miscellaneous internal changes

  • Entering a cancel scope whose deadline is in the past now immediately cancels it, so Cancelled will be raised by the first checkpoint in the cancel scope rather than the second one. This also affects constructs like with trio.move_on_after(0):. (#320)

Trio 0.10.0 (2019-01-07)

Features

Bugfixes

  • Fixed a race condition on macOS, where Trio’s TCP listener would crash if an incoming TCP connection was closed before the listener had a chance to accept it. (#609)

  • trio.open_tcp_stream() has been refactored to clean up unsuccessful connection attempts more reliably. (#809)

Deprecations and Removals

  • Remove the APIs deprecated in 0.5.0. (ClosedStreamError, ClosedListenerError, Result) (#812)

Miscellaneous internal changes

  • There are a number of methods on trio.ssl.SSLStream that report information about the negotiated TLS connection, like selected_alpn_protocol, and thus cannot succeed until after the handshake has been performed. Previously, we returned None from these methods, like the stdlib ssl module does, but this is confusing, because that can also be a valid return value. Now we raise trio.ssl.NeedHandshakeError instead. (#735)

Trio 0.9.0 (2018-10-12)

Features

  • New and improved APIs for inter-task communication: trio.abc.SendChannel, trio.abc.ReceiveChannel, and trio.open_memory_channel() (which replaces trio.Queue). This interface uses separate “sender” and “receiver” objects, for consistency with other communication interfaces like Stream. Also, the two objects can now be closed individually, making it much easier to gracefully shut down a channel. Also, check out the nifty clone API to make it easy to manage shutdown in multiple-producer/multiple-consumer scenarios. Also, the API has been written to allow for future channel implementations that send objects across process boundaries. Also, it supports unbounded buffering if you really need it. Also, help I can’t stop writing also. See Using channels to pass values between tasks for more details. (#497)

Deprecations and Removals

Trio 0.8.0 (2018-10-01)

Features

  • Trio’s default internal clock is now based on time.perf_counter() instead of time.monotonic(). This makes time-keeping more precise on Windows, and has no effect on other platforms. (#33)

  • Reworked trio, trio.testing, and trio.socket namespace construction, making them more understandable by static analysis tools. This should improve tab completion in editors, reduce false positives from pylint, and is a first step towards providing type hints. (#542)

Deprecations and Removals

Trio 0.7.0 (2018-09-03)

Features

  • The length of typical exception traces coming from Trio has been greatly reduced. This was done by eliminating many of the exception frames related to details of the implementation. For examples, see the blog post. (#56)

  • New and improved signal catching API: open_signal_receiver(). (#354)

  • The low level trio.hazmat.wait_socket_readable, wait_socket_writable, and notify_socket_close now work on bare socket descriptors, instead of requiring a socket.socket() object. (#400)

  • If you’re using trio.hazmat.wait_task_rescheduled and other low-level routines to implement a new sleeping primitive, you can now use the new trio.hazmat.Task.custom_sleep_data attribute to pass arbitrary data between the sleeping task, abort function, and waking task. (#616)

Bugfixes

  • Prevent crashes when used with Sentry (raven-python). (#599)

  • The nursery context manager was rewritten to avoid use of @asynccontextmanager and @async_generator. This reduces extraneous frames in exception traces and addresses bugs regarding StopIteration and StopAsyncIteration exceptions not propagating correctly. (#612)

  • Updates the formatting of exception messages raised by trio.open_tcp_stream() to correctly handle a hostname passed in as bytes, by converting the hostname to a string. (#633)

Deprecations and Removals

  • trio.catch_signals has been deprecated in favor of open_signal_receiver(). The main differences are: it takes *-args now to specify the list of signals (so open_signal_receiver(SIGINT) instead of catch_signals({SIGINT})), and, the async iterator now yields individual signals, instead of “batches” (#354)

  • Remove all the APIs deprecated in 0.3.0 and 0.4.0. (#623)

Trio 0.6.0 (2018-08-13)

Features

Bugfixes

  • Make trio.socket._SocketType.connect always close the socket on cancellation (#247)

  • Fix a memory leak in trio.CapacityLimiter, that could occur when acquire or acquire_on_behalf_of was cancelled. (#548)

  • Some version of macOS have a buggy getaddrinfo that was causing spurious test failures; we now detect those systems and skip the relevant test when found. (#580)

  • Prevent crashes when used with Sentry (raven-python). (#599)

Trio 0.5.0 (2018-07-20)

Features

  • Suppose one task is blocked trying to use a resource – for example, reading from a socket – and while it’s doing this, another task closes the resource. Previously, this produced undefined behavior. Now, closing a resource causes pending operations on that resource to terminate immediately with a ClosedResourceError. ClosedStreamError and ClosedListenerError are now aliases for ClosedResourceError, and deprecated. For this to work, Trio needs to know when a resource has been closed. To facilitate this, new functions have been added: trio.hazmat.notify_fd_close and trio.hazmat.notify_socket_close. If you’re using Trio’s built-in wrappers like SocketStream or trio.socket, then you don’t need to worry about this, but if you’re using the low-level functions like trio.hazmat.wait_readable, you should make sure to call these functions at appropriate times. (#36)

  • Tasks created by spawn_system_task() now no longer inherit the creator’s contextvars context, instead using one created at run(). (#289)

  • Add support for trio.Queue with capacity=0. Queue’s implementation is also faster now. (#473)

  • Switch to using standalone Outcome library for Result objects. (#494)

Deprecations and Removals

  • trio.hazmat.Result, trio.hazmat.Value and trio.hazmat.Error have been replaced by the equivalent classes in the Outcome library.

Trio 0.4.0 (2018-04-10)

Features

Bugfixes

  • Fix KeyboardInterrupt handling when threading state has been modified by a 3rd-party library. (#461)

Deprecations and Removals

Miscellaneous internal changes

Trio 0.3.0 (2017-12-28)

Features

  • Simplified nurseries: In Trio, the rule used to be that “parenting is a full time job”, meaning that after a task opened a nursery and spawned some children into it, it had to immediately block in __aexit__ to supervise the new children, or else exception propagation wouldn’t work. Also there was some elaborate machinery to let you replace this supervision logic with your own custom supervision logic. Thanks to new advances in task-rearing technology, parenting is no longer a full time job! Now the supervision happens automatically in the background, and essentially the body of a async with trio.open_nursery() block acts just like a task running inside the nursery. This is important: it makes it possible for libraries to abstract over nursery creation. For example, if you have a Websocket library that needs to run a background task to handle Websocket pings, you can now do that with async with open_websocket(...) as ws: ..., and that can run a task in the background without your users having to worry about parenting it. And don’t worry, you can still make custom supervisors; it turned out all that spiffy machinery was actually redundant and didn’t provide much value. (#136)

  • Trio socket methods like bind and connect no longer require “pre-resolved” numeric addresses; you can now pass regular hostnames and Trio will implicitly resolve them for you. (#377)

Bugfixes

  • Fixed some corner cases in Trio socket method implicit name resolution to better match stdlib behavior. Example: sock.bind(("", port)) now binds to the wildcard address instead of raising an error. (#277)

Deprecations and Removals

  • Removed everything that was deprecated in 0.2.0; see the 0.2.0 release notes below for details.

  • As was foretold in the v0.2.0 release notes, the bind method on Trio sockets is now async. Please update your calls or – better yet – switch to our shiny new high-level networking API, like serve_tcp(). (#241)

  • The resolve_local_address and resolve_remote_address methods on Trio sockets have been deprecated; these are unnecessary now that you can just pass your hostnames directly to the socket methods you want to use. (#377)

Trio 0.2.0 (2017-12-06)

Trio 0.2.0 contains changes from 14 contributors, and brings major new features and bug fixes, as well as a number of deprecations and a very small number of backwards incompatible changes. We anticipate that these should be easy to adapt to, but make sure to read about them below, and if you’re using Trio then remember to read and subscribe to issue #1.

Highlights

Breaking changes and deprecations

Trio is a young and ambitious project, but it also aims to become a stable, production-quality foundation for async I/O in Python. Therefore, our approach for now is to provide deprecation warnings where-ever possible, but on a fairly aggressive cycle as we push towards stability. If you use Trio you should read and subscribe to issue #1. We’d also welcome feedback on how this approach is working, whether our deprecation warnings could be more helpful, or anything else.

The tl;dr is: stop using socket.bind if you can, and then fix everything your test suite warns you about.

Upcoming breaking changes without warnings (i.e., stuff that works in 0.2.0, but won’t work in 0.3.0):

  • In the next release, the bind method on Trio socket objects will become async (#241). Unfortunately, there’s no good way to provide a warning here. We recommend switching to the new highlevel networking APIs like serve_tcp(), which will insulate you from this change.

Breaking changes (i.e., stuff that could theoretically break a program that worked on 0.1.0):

  • trio.socket no longer attempts to normalize or modernize socket options across different platforms. The high-level networking API now handles that, freeing trio.socket to focus on giving you raw, unadulterated BSD sockets.

  • When a socket sendall call was cancelled, it used to attach some metadata to the exception reporting how much data was actually sent. It no longer does this, because in common configurations like an SSLStream wrapped around a SocketStream it becomes ambiguous which “level” the partial metadata applies to, leading to confusion and bugs. There is no longer any way to tell how much data was sent after a sendall is cancelled.

  • The trio.socket.getprotobyname() function is now async, like it should have been all along. I doubt anyone will ever use it, but that’s no reason not to get the details right.

  • The trio.socket functions getservbyport, getservbyname, and getfqdn have been removed, because they were obscure, buggy, and obsolete. Use getaddrinfo() instead.

Upcoming breaking changes with warnings (i.e., stuff that in 0.2.0 will work but will print loud complaints, and that won’t work in 0.3.0):

  • For consistency with the new start method, the nursery spawn method is being renamed to start_soon (#284)

  • trio.socket.sendall is deprecated; use trio.open_tcp_stream and SocketStream.send_all instead (#291)

  • Trio now consistently uses run for functions that take and run an async function (like trio.run()!), and run_sync for functions that take and run a synchronous function. As part of this:

    • run_in_worker_thread is becoming run_sync_in_worker_thread

    • We took the opportunity to refactor run_in_trio_thread and await_in_trio_thread into the new class trio.BlockingTrioPortal

    • The hazmat function current_call_soon_thread_and_signal_safe is being replaced by trio.hazmat.TrioToken

    See #68 for details.

  • trio.Queue's join and task_done methods are deprecated without replacement (#321)

  • Trio 0.1.0 provided a set of built-in mechanisms for waiting for and tracking the result of individual tasks. We haven’t yet found any cases where using this actually led to simpler code, though, and this feature is blocking useful improvements, so the following are being deprecated without replacement:

    • nursery.zombies

    • nursery.monitor

    • nursery.reap

    • nursery.reap_and_unwrap

    • task.result

    • task.add_monitor

    • task.discard_monitor

    • task.wait

    This also lets us move a number of lower-level features out of the main trio namespace and into trio.hazmat:

    • trio.Tasktrio.hazmat.Task

    • trio.current_tasktrio.hazmat.current_task

    • trio.Resulttrio.hazmat.Result

    • trio.Valuetrio.hazmat.Value

    • trio.Errortrio.hazmat.Error

    • trio.UnboundedQueuetrio.hazmat.UnboundedQueue

    In addition, several introspection attributes are being renamed:

    • nursery.childrennursery.child_tasks

    • task.parent_task → use task.parent_nursery.parent_task instead

    See #136 for more details.

  • To consolidate introspection functionality in trio.hazmat, the following functions are moving:

    See #317 for more details.

  • It was decided that 0.1.0’s “yield point” terminology was confusing; we now use “checkpoint” instead. As part of this, the following functions in trio.hazmat are changing names:

    In addition, the following functions in trio.testing are changing names:

    See #157 for more details.

  • trio.format_exception is deprecated; use traceback.format_exception() instead (#347).

  • trio.current_instruments is deprecated. For adding or removing instrumentation at run-time, see trio.hazmat.add_instrument and trio.hazmat.remove_instrument (#257)

Unfortunately, a limitation in PyPy3 5.8 breaks our deprecation handling for some renames. (Attempting to use the old names will give an unhelpful error instead of a helpful warning.) This does not affect CPython, or PyPy3 5.9+.

Other changes

  • run_sync_in_worker_thread now has a robust mechanism for applying capacity limits to the number of concurrent threads (#10, #57, #156)

  • New support for tests to cleanly hook hostname lookup and socket operations: see Virtual networking for testing. In addition, trio.socket.SocketType is now an empty abstract base class, with the actual socket class made private. This shouldn’t effect anyone, since the only thing you could directly use it for in the first place was isinstance checks, and those still work (#170)

  • New class StrictFIFOLock

  • New exception ResourceBusyError

  • The trio.hazmat.ParkingLot class (which is used to implement many of Trio’s synchronization primitives) was rewritten to be simpler and faster (#272, #287)

  • It’s generally true that if you’re using Trio you have to use Trio functions, if you’re using asyncio you have to use asyncio functions, and so forth. (See the discussion of the “async sandwich” in the Trio tutorial for more details.) So for example, this isn’t going to work:

    async def main():
        # asyncio here
        await asyncio.sleep(1)
    
    # trio here
    trio.run(main)
    

    Trio now reliably detects if you accidentally do something like this, and gives a helpful error message.

  • Trio now also has special error messages for several other common errors, like doing trio.run(some_func()) (should be trio.run(some_func)).

  • trio.socket now handles non-ascii domain names using the modern IDNA 2008 standard instead of the obsolete IDNA 2003 standard (#11)

  • When an Instrument raises an unexpected error, we now route it through the logging module instead of printing it directly to stderr. Normally this produces exactly the same effect, but this way it’s more configurable. (#306)

  • Fixed a minor race condition in IOCP thread shutdown on Windows (#81)

  • Control-C handling on Windows now uses signal.set_wakeup_fd() and should be more reliable (#42)

  • trio.run() takes a new keyword argument restrict_keyboard_interrupt_to_checkpoints

  • New attributes allow more detailed introspection of the task tree: nursery.child_tasks, Task.child_nurseries, nursery.parent_task, Task.parent_nursery

  • trio.testing.wait_all_tasks_blocked() now takes a tiebreaker= argument. The main use is to allow MockClock's auto-jump functionality to avoid interfering with direct use of wait_all_tasks_blocked() in the same test.

  • MultiError.catch() now correctly preserves __context__, despite Python’s best attempts to stop us (#165)

  • It is now possible to take weakrefs to Lock and many other classes (#331)

  • Fix sock.accept() for IPv6 sockets (#164)

  • PyCharm (and hopefully other IDEs) can now offer better completions for the trio and trio.hazmat modules (#314)

  • Trio now uses yapf to standardize formatting across the source tree, so we never have to think about whitespace again.

  • Many documentation improvements

Trio 0.1.0 (2017-03-10)

  • Initial release.

Preparing a release

Things to do for releasing:

  • announce intent to release on gitter

  • check for open issues / pull requests that really should be in the release

    • come back when these are done

    • … or ignore them and do another release next week

  • check for deprecations “long enough ago” (two months or two releases, whichever is longer)

    • remove affected code

  • Do the actual release changeset

    • bump version number

      • increment as per Semantic Versioning rules

      • remove +dev tag from version number

    • Run towncrier

      • review history change

      • git rm changes

    • commit

  • push to your personal repository

  • create pull request to python-trio/trio’s “master” branch

  • verify that all checks succeeded

  • tag with vVERSION, push tag on python-trio/trio (not on your personal repository)

  • push to PyPI:

    git clean -xdf   # maybe run 'git clean -xdn' first to see what it will delete
    python3 setup.py sdist bdist_wheel
    twine upload dist/*
    
  • update version number in the same pull request

    • add +dev tag to the end

  • merge the release pull request

  • announce on gitter

Code of Conduct

This code of conduct applies to the Trio project, and all associated projects in the python-trio organization.

When Something Happens

If you see a Code of Conduct violation, follow these steps:

  1. Let the person know that what they did is not appropriate and ask them to stop and/or edit their message(s) or commits.

  2. That person should immediately stop the behavior and correct the issue.

  3. If this doesn’t happen, or if you’re uncomfortable speaking up, contact the maintainers.

  4. As soon as possible, a maintainer will look into the issue, and take further action (see below), starting with a warning, then temporary block, then long-term repo or organization ban.

When reporting, please include any relevant details, links, screenshots, context, or other information that may be used to better understand and resolve the situation.

The maintainer team will prioritize the well-being and comfort of the recipients of the violation over the comfort of the violator. See some examples below.

Our Pledge

In the interest of fostering an open and welcoming environment, we as contributors and maintainers of this project pledge to making participation in our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, technical preferences, nationality, personal appearance, race, religion, or sexual identity and orientation.

Our Standards

Examples of behavior that contributes to creating a positive environment include:

  • Using welcoming and inclusive language.

  • Being respectful of differing viewpoints and experiences.

  • Gracefully accepting constructive feedback.

  • Focusing on what is best for the community.

  • Showing empathy and kindness towards other community members.

  • Encouraging and raising up your peers in the project so you can all bask in hacks and glory.

Examples of unacceptable behavior by participants include:

  • The use of sexualized language or imagery and unwelcome sexual attention or advances, including when simulated online. The only exception to sexual topics is channels/spaces specifically for topics of sexual identity.

  • Casual mention of slavery or indentured servitude and/or false comparisons of one’s occupation or situation to slavery. Please consider using or asking about alternate terminology when referring to such metaphors in technology.

  • Making light of/making mocking comments about trigger warnings and content warnings.

  • Trolling, insulting/derogatory comments, and personal or political attacks.

  • Public or private harassment, deliberate intimidation, or threats.

  • Publishing others’ private information, such as a physical or electronic address, without explicit permission. This includes any sort of “outing” of any aspect of someone’s identity without their consent.

  • Publishing private screenshots or quotes of interactions in the context of this project without all quoted users’ explicit consent.

  • Publishing of private communication that doesn’t have to do with reporting harassment.

  • Any of the above even when presented as “ironic” or “joking”.

  • Any attempt to present “reverse-ism” versions of the above as violations. Examples of reverse-isms are “reverse racism”, “reverse sexism”, “heterophobia”, and “cisphobia”.

  • Unsolicited explanations under the assumption that someone doesn’t already know it. Ask before you teach! Don’t assume what people’s knowledge gaps are.

  • Feigning or exaggerating surprise when someone admits to not knowing something.

  • Well-actuallies

  • Other conduct which could reasonably be considered inappropriate in a professional or community setting.

Scope

This Code of Conduct applies both within spaces involving this project and in other spaces involving community members. This includes the repository, its Pull Requests and Issue tracker, its Twitter community, private email communications in the context of the project, and any events where members of the project are participating, as well as adjacent communities and venues affecting the project’s members.

Depending on the violation, the maintainers may decide that violations of this code of conduct that have happened outside of the scope of the community may deem an individual unwelcome, and take appropriate action to maintain the comfort and safety of its members.

Other Community Standards

As a project on GitHub, this project is additionally covered by the GitHub Community Guidelines.

Enforcement of those guidelines after violations overlapping with the above are the responsibility of the entities, and enforcement may happen in any or all of the services/communities.

Maintainer Enforcement Process

Once the maintainers get involved, they will follow a documented series of steps and do their best to preserve the well-being of project members. This section covers actual concrete steps.

Contacting Maintainers

As a small and young project, we don’t yet have a Code of Conduct enforcement team. Hopefully that will be addressed as we grow, but for now, any issues should be addressed to Nathaniel J. Smith, via email or any other medium that you feel comfortable with. Using words like “Trio code of conduct” in your subject will help make sure your message is noticed quickly.

Further Enforcement

If you’ve already followed the initial enforcement steps, these are the steps maintainers will take for further enforcement, as needed:

  1. Repeat the request to stop.

  2. If the person doubles down, they will have offending messages removed or edited by a maintainers given an official warning. The PR or Issue may be locked.

  3. If the behavior continues or is repeated later, the person will be blocked from participating for 24 hours.

  4. If the behavior continues or is repeated after the temporary block, a long-term (6-12mo) ban will be used.

  5. If after this the behavior still continues, a permanent ban may be enforced.

On top of this, maintainers may remove any offending messages, images, contributions, etc, as they deem necessary.

Maintainers reserve full rights to skip any of these steps, at their discretion, if the violation is considered to be a serious and/or immediate threat to the health and well-being of members of the community. These include any threats, serious physical or verbal attacks, and other such behavior that would be completely unacceptable in any social setting that puts our members at risk.

Members expelled from events or venues with any sort of paid attendance will not be refunded.

Who Watches the Watchers?

Maintainers and other leaders who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project’s leadership. These may include anything from removal from the maintainer team to a permanent ban from the community.

Additionally, as a project hosted on GitHub, their Code of Conduct may be applied against maintainers of this project, externally of this project’s procedures.

Enforcement Examples

The Best Case

The vast majority of situations work out like this. This interaction is common, and generally positive.

Alex: “Yeah I used X and it was really crazy!”

Patt (not a maintainer): “Hey, could you not use that word? What about ‘ridiculous’ instead?”

Alex: “oh sorry, sure.” -> edits old comment to say “it was really confusing!”

The Maintainer Case

Sometimes, though, you need to get maintainers involved. Maintainers will do their best to resolve conflicts, but people who were harmed by something will take priority.

Patt: “Honestly, sometimes I just really hate using $library and anyone who uses it probably sucks at their job.”

Alex: “Whoa there, could you dial it back a bit? There’s a CoC thing about attacking folks’ tech use like that.”

Patt: “I’m not attacking anyone, what’s your problem?”

Alex: “@maintainers hey uh. Can someone look at this issue? Patt is getting a bit aggro. I tried to nudge them about it, but nope.”

KeeperOfCommitBits: (on issue) “Hey Patt, maintainer here. Could you tone it down? This sort of attack is really not okay in this space.”

Patt: “Leave me alone I haven’t said anything bad wtf is wrong with you.”

KeeperOfCommitBits: (deletes user’s comment), “@patt I mean it. Please refer to the CoC over at (URL to this CoC) if you have questions, but you can consider this an actual warning. I’d appreciate it if you reworded your messages in this thread, since they made folks there uncomfortable. Let’s try and be kind, yeah?”

Patt: “@keeperofbits Okay sorry. I’m just frustrated and I’m kinda burnt out and I guess I got carried away. I’ll DM Alex a note apologizing and edit my messages. Sorry for the trouble.”

KeeperOfCommitBits: “@patt Thanks for that. I hear you on the stress. Burnout sucks :/. Have a good one!”

The Nope Case

PepeTheFrog🐸: “Hi, I am a literal actual nazi and I think white supremacists are quite fashionable.”

Patt: “NOOOOPE. OH NOPE NOPE.”

Alex: “JFC NO. NOPE. @keeperofbits NOPE NOPE LOOK HERE”

KeeperOfCommitBits: “👀 Nope. NOPE NOPE NOPE. 🔥”

PepeTheFrog🐸 has been banned from all organization or user repositories belonging to KeeperOfCommitBits.

Attribution

This Code of Conduct was generated using WeAllJS Code of Conduct Generator, which is based on the WeAllJS Code of Conduct, which is itself based on Contributor Covenant, version 1.4, available at http://contributor-covenant.org/version/1/4, and the LGBTQ in Technology Slack Code of Conduct.

Indices and tables