1. Code
  2. Python

Asynchronous I/O With Python 3

Scroll to top
9 min read

In this tutorial you'll go through a whirlwind tour of the asynchronous I/O facilities introduced in Python 3.4 and improved further in Python 3.5 and 3.6. 

Python previously had few great options for asynchronous programming. The new Async I/O support finally brings first-class support that includes both high-level APIs and standard support that aims to unify multiple third-party solutions (Twisted, Gevent, Tornado, asyncore, etc.).

It's important to understand that learning Python's async IO is not trivial due to the rapid iteration, the scope, and the need to provide a migration path to existing async frameworks. I'll focus on the latest and greatest to simplify a little.

There are many moving parts that interact in interesting ways across thread boundaries, process boundaries, and remote machines. There are platform-specific differences and limitations. Let's jump right in. 

Pluggable Event Loops

The core concept of async IO is the event loop. In a program, there may be multiple event loops. Each thread will have at most one active event loop. The event loop provides the following facilities:

  • Registering, executing and cancelling delayed calls (with timeouts).
  • Creating client and server transports for various kinds of communication.
  • Launching subprocesses and the associated transports for communication with an external program.
  • Delegating costly function calls to a pool of threads. 

Quick Example

Here is a little example that starts two coroutines and calls a function in delay. It shows how to use an event loop to power your program:

1
import asyncio
2
3
4
async def foo(delay):
5
    for i in range(10):
6
        print(i)
7
        await asyncio.sleep(delay)
8
9
10
def stopper(loop):
11
    loop.stop()
12
13
14
loop = asyncio.get_event_loop()
15
16
# Schedule a call to foo()

17
loop.create_task(foo(0.5))
18
loop.create_task(foo(1))
19
loop.call_later(12, stopper, loop)
20
21
# Block until loop.stop() is called()

22
loop.run_forever()
23
loop.close()

The AbstractEventLoop class provides the basic contract for event loops. There are many things an event loop needs to support:

  • Scheduling functions and coroutines for execution
  • Creating futures and tasks
  • Managing TCP servers
  • Handling signals (on Unix)
  • Working with pipes and subprocesses

Here are the methods related to running and stopping the event as well as scheduling functions and coroutines:

1
class AbstractEventLoop:
2
    """Abstract event loop."""
3
4
    # Running and stopping the event loop.

5
6
    def run_forever(self):
7
        """Run the event loop until stop() is called."""
8
        raise NotImplementedError
9
10
    def run_until_complete(self, future):
11
        """Run the event loop until a Future is done.

12


13
        Return the Future's result, or raise its exception.

14
        """
15
        raise NotImplementedError
16
17
    def stop(self):
18
        """Stop the event loop as soon as reasonable.

19


20
        Exactly how soon that is may depend on the implementation, but

21
        no more I/O callbacks should be scheduled.

22
        """
23
        raise NotImplementedError
24
25
    def is_running(self):
26
        """Return whether the event loop is currently running."""
27
        raise NotImplementedError
28
29
    def is_closed(self):
30
        """Returns True if the event loop was closed."""
31
        raise NotImplementedError
32
33
    def close(self):
34
        """Close the loop.

35


36
        The loop should not be running.

37


38
        This is idempotent and irreversible.

39


40
        No other methods should be called after this one.

41
        """
42
        raise NotImplementedError
43
44
    def shutdown_asyncgens(self):
45
        """Shutdown all active asynchronous generators."""
46
        raise NotImplementedError
47
48
    # Methods scheduling callbacks.  All these return Handles.

49
50
    def _timer_handle_cancelled(self, handle):
51
        """Notification that a TimerHandle has been cancelled."""
52
        raise NotImplementedError
53
54
    def call_soon(self, callback, *args):
55
        return self.call_later(0, callback, *args)
56
57
    def call_later(self, delay, callback, *args):
58
        raise NotImplementedError
59
60
    def call_at(self, when, callback, *args):
61
        raise NotImplementedError
62
63
    def time(self):
64
        raise NotImplementedError
65
66
    def create_future(self):
67
        raise NotImplementedError
68
69
    # Method scheduling a coroutine object: create a task.

70
71
    def create_task(self, coro):
72
        raise NotImplementedError
73
74
    # Methods for interacting with threads.

75
76
    def call_soon_threadsafe(self, callback, *args):
77
        raise NotImplementedError
78
79
    def run_in_executor(self, executor, func, *args):
80
        raise NotImplementedError
81
82
    def set_default_executor(self, executor):
83
        raise NotImplementedError

Plugging in a new Event Loop

Asyncio is designed to support multiple implementations of event loops that adhere to its API. The key is the EventLoopPolicy class that configures asyncio and allows the controlling of every aspect of the event loop. Here is an example of a custom event loop called uvloop based on the libuv, which is supposed to be much faster that the alternatives (I haven't benchmarked it myself):

1
import asyncio
2
import uvloop
3
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

That's it. Now, whenever you use any asyncio function, it's uvloop under the covers.

Coroutines, Futures, and Tasks

A coroutine is a loaded term. It is both a function that executes asynchronously and an object that needs to be scheduled. You define them by adding the async keyword before the definition:

1
import asyncio
2
3
4
async def cool_coroutine():
5
    return "So cool..."
6

If you call such a function, it doesn't run. Instead, it returns a coroutine object, and if you don't schedule it for execution then you'll get a warning too:

1
c = cool_coroutine()
2
print(c)
3
4
Output:
5
6
<coroutine object cool_coroutine at 0x108a862b0>
7
sys:1: RuntimeWarning: coroutine 'cool_coroutine' was never awaited
8
9
Process finished with exit code 0

To actually execute the coroutine, we need an event loop:

1
r = loop.run_until_complete(c)
2
loop.close()
3
4
print(r)
5
6
Output:
7
8
So cool...

That's direct scheduling. You can also chain coroutines. Note that you have to call await when invoking coroutines:

1
import asyncio
2
3
async def compute(x, y):
4
    print("Compute %s + %s ..." % (x, y))
5
    await asyncio.sleep(1.0)
6
    return x + y
7
8
async def print_sum(x, y):
9
    result = await compute(x, y)
10
    print("%s + %s = %s" % (x, y, result))
11
12
loop = asyncio.get_event_loop()
13
loop.run_until_complete(print_sum(1, 2))
14
loop.close()

The asyncio Future class is similar to the concurrent.future.Future class. It is not threadsafe and supports the following features:

  • adding and removing done callbacks
  • cancelling
  • setting results and exceptions

Here is how to use a future with the event loop. The take_your_time() coroutine accepts a future and sets its result after sleeping for a second.

The ensure_future() function schedules the coroutine, and wait_until_complete() waits for the future to be done. Behind the curtain, it adds a done callback to the future.

1
import asyncio
2
3
async def take_your_time(future):
4
    await asyncio.sleep(1)
5
    future.set_result(42)
6
7
loop = asyncio.get_event_loop()
8
future = asyncio.Future()
9
asyncio.ensure_future(take_your_time(future))
10
loop.run_until_complete(future)
11
print(future.result())
12
loop.close()

This is pretty cumbersome. Asyncio provides tasks to make working with futures and coroutines more pleasant. A Task is a subclass of Future that wraps a coroutine and that you can cancel. 

The coroutine doesn't have to accept an explicit future and set its result or exception. Here is how to perform the same operations with a task:

1
import asyncio
2
3
async def take_your_time():
4
    await asyncio.sleep(1)
5
    return 42
6
7
loop = asyncio.get_event_loop()
8
task = loop.create_task(take_your_time())
9
loop.run_until_complete(task)
10
print(task.result())
11
loop.close()

Transports, Protocols, and Streams

A transport is an abstraction of a communication channel. A transport always supports a particular protocol. Asyncio provides built-in implementations for TCP, UDP, SSL, and subprocess pipes.

If you're familiar with socket-based network programming then you'll feel right at home with transports and protocols. With Asyncio, you get asynchronous network programming in a standard way. Let's look at the infamous echo server and client (the "hello world" of networking). 

First, the echo client implements a class called EchoClient that is derived from the asyncio.Protocol. It keeps its event loop and a message it will send to the server upon connection. 

In the connection_made() callback, it writes its message to the transport. In the data_received() method, it just prints the server's response, and in the connection_lost() method it stops the event loop. When passing an instance of the EchoClient class to the loop's create_connection() method, the result is a coroutine that the loop runs until it completes. 

1
import asyncio
2
3
class EchoClient(asyncio.Protocol):
4
    def __init__(self, message, loop):
5
        self.message = message
6
        self.loop = loop
7
8
    def connection_made(self, transport):
9
        transport.write(self.message.encode())
10
        print('Data sent: {!r}'.format(self.message))
11
12
    def data_received(self, data):
13
        print('Data received: {!r}'.format(data.decode()))
14
15
    def connection_lost(self, exc):
16
        print('The server closed the connection')
17
        print('Stop the event loop')
18
        self.loop.stop()
19
20
loop = asyncio.get_event_loop()
21
message = 'Hello World!'
22
coro = loop.create_connection(lambda: EchoClient(message, loop),
23
                              '127.0.0.1', 8888)
24
loop.run_until_complete(coro)
25
loop.run_forever()
26
loop.close()  

The server is similar except that it runs forever, waiting for clients to connect. After it sends an echo response, it also closes the connection to the client and is ready for the next client to connect. 

A new instance of the EchoServer is created for each connection, so even if multiple clients connect at the same time, there will be no problem of conflicts with the transport attribute.

1
import asyncio
2
3
class EchoServer(asyncio.Protocol):
4
    def connection_made(self, transport):
5
        peername = transport.get_extra_info('peername')
6
        print('Connection from {}'.format(peername))
7
        self.transport = transport
8
9
    def data_received(self, data):
10
        message = data.decode()
11
        print('Data received: {!r}'.format(message))
12
13
        print('Send: {!r}'.format(message))
14
        self.transport.write(data)
15
16
        print('Close the client socket')
17
        self.transport.close()
18
19
loop = asyncio.get_event_loop()
20
# Each client connection will create a new protocol instance

21
coro = loop.create_server(EchoServer, '127.0.0.1', 8888)
22
server = loop.run_until_complete(coro)
23
print('Serving on {}'.format(server.sockets[0].getsockname()))
24
loop.run_forever()

Here is the output after two clients connected:

1
Serving on ('127.0.0.1', 8888)
2
Connection from ('127.0.0.1', 53248)
3
Data received: 'Hello World!'
4
Send: 'Hello World!'
5
Close the client socket
6
Connection from ('127.0.0.1', 53351)
7
Data received: 'Hello World!'
8
Send: 'Hello World!'
9
Close the client socket

Streams provide a high-level API that is based on coroutines and provides Reader and Writer abstractions. The protocols and the transports are hidden, there is no need to define your own classes, and there are no callbacks. You just await events like connection and data received. 

The client calls the open_connection() function that returns the reader and writer objects used naturally. To close the connection, it closes the writer. 

1
import asyncio
2
3
4
async def tcp_echo_client(message, loop):
5
    reader, writer = await asyncio.open_connection(
6
        '127.0.0.1', 
7
        8888, 
8
        loop=loop)
9
10
    print('Send: %r' % message)
11
    writer.write(message.encode())
12
13
    data = await reader.read(100)
14
    print('Received: %r' % data.decode())
15
16
    print('Close the socket')
17
    writer.close()
18
19
20
message = 'Hello World!'
21
loop = asyncio.get_event_loop()
22
loop.run_until_complete(tcp_echo_client(message, loop))
23
loop.close()

The server is also much simplified.

1
import asyncio
2
3
async def handle_echo(reader, writer):
4
    data = await reader.read(100)
5
    message = data.decode()
6
    addr = writer.get_extra_info('peername')
7
    print("Received %r from %r" % (message, addr))
8
9
    print("Send: %r" % message)
10
    writer.write(data)
11
    await writer.drain()
12
13
    print("Close the client socket")
14
    writer.close()
15
16
loop = asyncio.get_event_loop()
17
coro = asyncio.start_server(handle_echo, 
18
                            '127.0.0.1', 
19
                            8888, 
20
                            loop=loop)
21
server = loop.run_until_complete(coro)
22
print('Serving on {}'.format(server.sockets[0].getsockname()))
23
loop.run_forever()

Working With Sub-Processes

Asyncio covers interactions with sub-processes too. The following program launches another Python process and executes the code "import this". It is one of Python's famous Easter eggs, and it prints the "Zen of Python". Check out the output below. 

The Python process is launched in the zen() coroutine using the create_subprocess_exec() function and binds the standard output to a pipe. Then it iterates over the standard output line by line using await to give other processes or coroutines a chance to execute if output is not ready yet. 

Note that on Windows you have to set the event loop to the ProactorEventLoop because the standard SelectorEventLoop doesn't support pipes. 

1
import asyncio.subprocess
2
import sys
3
4
5
async def zen():
6
    code = 'import this'
7
    create = asyncio.create_subprocess_exec(
8
        sys.executable, 
9
        '-c', 
10
        code,
11
        stdout=asyncio.subprocess.PIPE)
12
    proc = await create
13
14
    data = await proc.stdout.readline()
15
    while data:
16
        line = data.decode('ascii').rstrip()
17
        print(line)
18
        data = await proc.stdout.readline()
19
20
    await proc.wait()
21
22
if sys.platform == "win32":
23
    loop = asyncio.ProactorEventLoop()
24
    asyncio.set_event_loop(loop)
25
else:
26
    loop = asyncio.get_event_loop()
27
28
loop.run_until_complete(zen())
29
30
Output:
31
32
The Zen of Python, by Tim Peters
33
34
Beautiful is better than ugly.
35
Explicit is better than implicit.
36
Simple is better than complex.
37
Complex is better than complicated.
38
Flat is better than nested.
39
Sparse is better than dense.
40
Readability counts.
41
Special cases aren't special enough to break the rules.

42
Although practicality beats purity.

43
Errors should never pass silently.

44
Unless explicitly silenced.

45
In the face of ambiguity, refuse the temptation to guess.

46
There should be one-- and preferably only one --obvious way to

47
do it.

48
Although that way may not be obvious at first unless you're
49
Dutch.
50
Now is better than never.
51
Although never is often better than *right* now.
52
If the implementation is hard to explain, it's a bad idea.

53
If the implementation is easy to explain, it may be a good idea.

54
Namespaces are one honking great idea -- let's do more of those!

Conclusion

Don’t hesitate to see what we have available for sale and for study in the marketplace, and don't hesitate to ask any questions and provide your valuable feedback using the feed below.

Python's asyncio is a comprehensive framework for asynchronous programming. It has a huge scope and supports both low-level as well as high-level APIs. It is still relatively young and not well understood by the community. 

I'm confident that over time best practices will emerge, and more examples will surface and make it easier to use this powerful library.

Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.