02 Feb 2020
In this post we’ll cover Python coroutines. First we’ll study David Beazley’s great tutorial  to learn about generators and coroutines, and how to build a multitask system from the ground-up.
We’ll then take a look at a Python module called asyncio which makes use of coroutines to enable asynchronous I/O.
The syntax for yielding the execution back is via the
yield keyword. Here’s an example of a custom implementation of
Because the execution can be yielded at any time in the code, we can use infinite loops:
Another way to use generators is to “pipe” (in the Linux way), one generator into another, like in the example where we read lines from a dictionary file and find those ending in “ion”:
This also highlights one advantage of using generators abstraction: the lower memory footprint since it can process one line at a time. Sure, we could achieve the same with regular functions, but they would need to be combined in a single block:
Which is efficient but doesn’t allow modularity.
Combined with the fact that generators can be infinite loops, one can model functionality like tail and grep as generators (e.g. tail -f | grep foo) which is the exact example Beazley provides in his presentation .
In PEP 342 it explains the motivation behind coroutines:
Coroutines are a natural way of expressing many algorithms, such as simulations, games, asynchronous I/O, and other forms of event-driven programming or co-operative multitasking. Python’s generator functions are almost coroutines – but not quite – in that they allow pausing execution to produce a value, but do not provide for values or exceptions to be passed in when execution resumes. They also do not allow execution to be paused within the
try/finallyblocks, and therefore make it difficult for an aborted coroutine to clean up after itself. There are four main changes:
yieldis an expression instead of statement, which means you can assign the return type of yield to a variable.
send()method which can be used to send value to the yield expression.
send()is a general version of
next(), which is equivalent to
throw()method which raises an exception at the place where the last yield was called.
close()method which raises a
GeneratorExitat the place where the last yield was called. Effectively used to force terminating the generator. Here’s a simple coroutine that just prints what it receives:
Notice that we have to call an empty
send() to “initialize” the coroutine. According to PEP 342:
Because generator-iterators begin execution at the top of the generator’s function body, there is no yield expression to receive a value when the generator has just been created. Therefore, calling send() with a non-None argument is prohibited when the generator iterator has just started (…). Thus, before you can communicate with a coroutine you must first call next() or send(None) to advance its execution to the first yield expression. In the example below we highlight how this flow can be confusing. The first
send()receives the value from yield, but it is only able to send a value through a subsequent
Since in a coroutine most of the times the first
send() will be to initialize the coroutine, Beazley provides a decorator to abstract that:
We can chain multiple coroutines the same we did with generators but now the order is somewhat reversed. To see why, consider our generator pipe example rewritten with coroutines. In here we pass
get_words(). Contrast this with the generator version where we start with get_words, pass its results to
filter_step() and then
Beazley provides an example using an XML parser to manipulate XML and convert to JSON. The API of xml.sax relies on the visitor pattern, where at important steps of the AST traversal (e.g. startElement, character, endElement) it calls specific methods of a content handler.
The example uses an adapter layer (cosax.py), to allow using coroutines instead the content handler. It also combines multiple smaller coroutines to produce a more complex ones, demonstrating the composability of coroutines.
After some preliminary overview of Operating Systems Beazley builds a prototype of a multitasking system by modeling coroutines as tasks (no threads or processes).
One of the interesting implementation of the multitasking system is system calls. The task/coroutine issues a system call by providing it as a value to the yield (e.g.
yield GetTid()). This is received by the scheduler which can provide the necessary context to the specific system call implementation.
The tutorial then covers one of the most important parts of the multitasking system which is the ability to do asynchronous I/O operations (e.g. reading and writing to a file).
The idea is to introduce a “system call” corresponding to performing I/O. When a task invokes that, it doesn’t get rescheduled but put in a staging area (
self.read_waiting in the code below). The scheduler has then a continuously run task that polls the OS to check if any of the file descriptors corresponding to the tasks in the staging area are ready, in which case the task is rescheduled.
Beazley uses that system to implement a simple server that receives data via a socket:
One of the limitations of the scheduler that is developed up to this point is that the
yield has to happen at the top-level of coroutine, that is, it cannot invoke other functions that yield, which limits refactoring and modularization.
To overcome this limitation, the author proposes a technique called trampolining. Here’s an example where the execution of
add() is done by the top level code, not by
We can do a similar thing in the multitasking system. Because the coroutines can recursively call other coroutines, we need to keep a callstack. We can do it inside
Task by keeping an explicit stack:
In the example above, when a coroutine that is wrapped in
Task makes another coroutine call, for example:
client, addr = yield Accept(sock)
result will be a generator, so the current coroutine will be put on the stack and the execution will pass to that generator instead:
self.target = result
When that sub-coroutine yields the execution, the original caller is resumed
self.target = self.stack.pop()
Note that this all happen transparently to the scheduler itself.
This concludes the study of the multitask system from the ground up.
The tutorial we just studied is from 2009 and while still relevant, Python went through many changes since then.
We’ll now cover some of the development since the generator-based coroutines were introduced and that culminated in a standard module for handling asynchronous I/O.
PEP 380 introduced a new syntax for delegating the execution to another generator until that generator is finished. Simeon  provides an example where using yield from simplifies the code. In the code below
generator3() in a loop to “exhaust” their execution.
The same can be accomplished with yield for, which does that implicitly:
PEP 492 Introduces the async and await syntax. The motivation contains these two reasons among others:
It is easy to confuse coroutines with regular generators, since they share the same syntax; this is especially true for new developers. Whether or not a function is a coroutine is determined by a presence of yield or yield from statements in its body, which can lead to unobvious errors when such statements appear in or disappear from function body during refactoring To address these issues, a function containing the
asyncis considered a native coroutine (as opposed to a generator-based coroutine):
A native coroutine can await generator-based coroutines in which case it has the same behavior as the wait for. Borrowing from the example above:
PEP 3156 Describes a system to support asynchronous I/O, which is now known as the
asyncio module. It proposes a coroutine-based multitasking system similar to the one Beazley describes. It has support to common I/O operations like those involving files, sockets, subprocesses, etc.
We won’t dive into much detail in this post, but we can see parallels to the scheduler we studied previously. Here’s an example where
asyncio.run() schedules the coroutine
main() which in turn executes the sub-coroutines
sleep() one after another:
This is not taking advantage of the non-blocking capabilities of asyncio. Why? Recall that await is equivalent to yield from and that causes the current coroutine to wait the call to finish until it continues. If we run this code we get:
will sleep: 1
will sleep: 2
What we want is to schedule both sub-coroutines at the same time, and asyncio allows that via the
If we run this code we get:
will sleep 1
will sleep 2
Which means that the first sleep executed but yielded the execution back to the scheduler after the
sleep() call. The second
sleep() got run, printing “will sleep 2”.
David Beazley’s tutorial is really good. They’re thorough and provide lots of analogies with operating systems concepts.
I also liked the presentation medium: the slides are self-contained (all the information is present as text) and he shows the whole code at first then repeats the code multiple times in subsequent slides, highlighting the important pieces in each of them. This is difficult to achieve in flat text like a blog post.