Asynchronous Python for Web Development

Asynchronous programming is well suited for tasks that include reading and writing files frequently or sending data back and forth from a server. Asynchronous programs perform I/O operations in a non-blocking fashion, meaning that they can perform other tasks while waiting for data to return from a client rather than waiting idly, wasting resources and time.

Python, like many other languages, suffers from not being asynchronous by default. Fortunately, rapid changes in the IT world allow us to write asynchronous code even using languages that weren't originally meant to do so. Over the years, the demands for speed are exceeding hardware capabilities and companies around the world have come together with the Reactive Manifesto in order to deal with this issue.

The non-blocking behavior of asynchronous programs can result in significant performance benefits in the context of a web application, helping to address the issue of developing reactive apps.

Cooked into Python 3 are some powerful tools for writing asynchronous applications. In this article, we'll be covering some of these tools, especially as they relate to web development.

We'll be developing a simple reactive aiohttp based app to display the current relevant sky coordinates of planets from the Solar System, given the geographic coordinates of the user. You can find the app here, and the source code here.

We'll end up by discussing how to prepare the app to deploy to Heroku.

Introduction to Asynchronous Python

For those familiar with writing traditional Python code, making the jump to asynchronous code can be conceptually a little tricky. Asynchronous code in Python relies on coroutines, which in conjunction with an event loop allow for writing code that can appear to be doing more than one thing at a time.

Coroutines can be thought of as functions that have points in code where they give program control back to the calling context. These "yield" points allow for pausing and resuming coroutine execution, in addition to exchanging data between contexts.

The event loop decides what chunk of code runs at any given moment - it is responsible for pausing, resuming, and communicating between coroutines. This means that parts of different coroutines might end up executing in an order other than the one in which they were scheduled. This idea of running different chunks of code out of order is called concurrency.

Thinking about concurrency in the context of making HTTP requests can be elucidating. Imagine wanting to make many independent requests to a server. For example, we might want to query a website to get statistics about all the sports players in a given season.

We could make each request sequentially. However, with every request, we can imagine that out code might spend some time waiting around for a request to get delivered to the server, and for the response to be sent back.

Sometimes, these operations can take even multiple seconds. The application may experience network lag due to a high number of users, or simply due to the speed limits of the given server.

What if our code could do other things while waiting around for a response from the server? Moreover, what if it would only go back to processing a given request once response data arrived? We could make many requests in quick succession if we didn't have to wait for each individual request to finish before proceeding to the next in the list.

Coroutines with an event loop allow us to write code that behaves in exactly this manner.

asyncio

asyncio, part of the Python standard library, provides an event loop and a set of tools for controlling it. With asyncio we can schedule coroutines for execution, and create new coroutines (really asyncio.Task objects, using the parlance of asyncio) that will only finish executing once constituent coroutines finish executing.

Unlike other asynchronous programming languages, Python does not force us to use the event loop that ships with the language. As Brett Cannon points out, Python coroutines constitute an asynchronous API, with which we can use any event loop. Projects exist that implement a completely different event loop, like curio, or allow for dropping in a different event loop policy for asyncio (the event loop policy is what manages the event loop "behind the scenes"), like uvloop.

Let's take a look at a code snippet that runs two coroutines concurrently, each one printing out a message after one second:

This code executes in roughly 5 seconds, as the asyncio.sleep coroutine establishes points at which the event loop can jump to executing other code. Moreover, we've told the event loop to schedule both wait_around instances for concurrent execution with the asyncio.gather function.

asyncio.gather takes a list of "awaitables" (ie, coroutines, or asyncio.Task objects) and returns a single asyncio.Task object that only finishes when all its constituent tasks/coroutines are finished. The last two lines are asyncio boilerplate for running a given coroutine until its finished executing.

Coroutines, unlike functions, won't start executing immediately after they're invoked. The await keyword is what tells the event loop to schedule a coroutine for execution.

If we take out the await in front of asyncio.sleep, the program finishes (almost) instantly, as we haven't told the event loop to actually execute the coroutine, which in this case tells the coroutine to pause for a set amount of time.

With a grasp of what asynchronous Python code looks like, let's move on to asynchronous web development.

Installing aiohttp

aiohttp is a Python library for making asynchronous HTTP requests. In addition, it provides a framework for putting together the server part of a web application. Using Python 3.5+ and pip, we can install aiohttp:

pip install --user aiohttp

Client-Side: Making Requests

The following examples show how to we can download the HTML content of the "example.com" website using aiohttp:

async with is a context manager that works with coroutines instead of functions. In both cases in which it gets used, we can imagine that internally, aiohttp is closing down connections to servers or otherwise freeing up resources.

aiohttp.ClientSession has methods that correspond to HTTP verbs. In the same
way that session.get is making a GET request, session.post would make a POST request.

This example by itself offers no performance advantage over making synchronous HTTP requests. The real beauty of client-side aiohttp lies in making multiple concurrent requests:

Instead of making each request sequentially, we ask asyncio to do them concurrently, with asycio.gather.

PlanetTracker Web App

Through the course of this section, I intend to demonstrate how to put together an app that reports the current coordinates of planets in the sky at the user's location (ephemerides).

The user supplies his or her location with the web Geolocation API, which does the work for us.

I'll end up by showing how to set up a Procfile in order to deploy the app on Heroku. If you plan on following along as I work through putting the app together, you should do the following, assuming you have Python 3.6 and pip installed:

In order to make getting planet ephemerides easier, let's set up a class PlanetTracker with a method that returns a given planet's current azimith and altitude, in degrees (PyEphem defaults to using radians, not degrees, to represent angles internally):

Server-Side aiohttp: HTTP Routes

Given some latitude and longitude, we can easily get a planet's current ephemeris, in degrees. Now let's set up an aiohttp route to allow a client to get a planet's ephemeris given the user's geolocation.

Before we can start writing code, we have to think about what HTTP verbs we want to associate with each of these tasks. It makes sense to use POST for the first task, as we're setting the observer's geographic coordinates. Given that we're getting ephemerides, it makes sense to use GET for the second task:

When we run this, we can point our browser to our different routes to see the data our server returns. If I put localhost:8000/planets/mars into my browser's address bar, I should see some response like the following:

Note the presence of loop.run_forever instead of the call to loop.run_until_complete that we saw earlier. Instead of executing a set number of coroutines, we want our program to start a server that will handle requests until we exit with ctrl+c, at which point it will gracefully shutdown the server.

HTML/JavaScript Client

aiohttp allows us to serve up HTML and JavaScript files. Using aiohttp for serving "static" assets like CSS and JavaScript is discouraged, but for the purposes of this app, it shouldn't be an issue.

Let's add a few lines to our aiohttp_app.py file to serve up an HTML file that references a JavaScript file:

The hello coroutine is setting up a GET route at localhost:8000/ that serves up the contents of index.html, located in the same directory from which we run our server.

The app.router.add_static line is setting up a route at localhost:8000/ to serve up files in the same directory from which we run our server. This means that our browser will be able to find the JavaScript file we reference in index.html.

Note: In production, it makes sense to move HTML, CSS and JS files into a separate directory that gets served up on its own. This makes it so the curious user cannot access our server code.

This app will periodically (every 2 seconds) update and display planet ephemerides. We can supply our own geo coordinates, or let the Web Geolocation API determine our current location. The app updates the geolocation if the user stops typing for a half a second or more.

While this is not a JavaScript tutorial, I think it's useful to understand what different parts of the script are doing:

createPlanetDisplay is dynamically creating HTML elements and binding them to the Document Object Model (DOM)

updatePlanetDisplay takes data received from the server and populates the elements created by createPlanetDisplay

get makes a GET request to the server. The XMLHttpRequest object allows this to be done without reloading the page.

post makes a POST request to the server. Like with get this is done without reloading the page.

getGeoLocation uses the Web Geolocation API to get the user's current geographic coordinates. This must be fulfilled "in a secure context" (ie we must be using HTTPSnotHTTP).

getPlanetEphemeris and getPlanetEphemerides make GET requests to the server to get ephemeris for a specific planet and to get ephemerides for all planets, respectively.

testPerformance makes n requests to the server, and determines how long it takes.

Primer on Deploying to Heroku

Heroku is a service for easily deploying web applications. Heroku takes care of configuring web-facing components of an application, like configuring reverse proxies or worrying about load balancing. For applications handling few requests and a small number of users, Heroku is a great free hosting service.

Deploying Python applications to Heroku has become very easy in recent years. At its core, we have to create two files that list our application's dependencies and tell Heroku how to run our application.

A Pipfile takes care of the former, while a Procfile takes care of the latter. A Pipfile is maintained by using pipenv - we add to our Pipfile (and Pipfile.lock) every time we install a dependency.

In order to run our app on Heroku, we have to add one more dependency:

Now you can follow the instructions on the Heroku devcenter here for deploying your app. Note that you can skip the "Prepare the App" step of this tutorial, as you already have a git tracked app.

Once your application is deployed, you can navigate to the chosen Heroku URL in your browser and view the app, which will look something like this:

Conclusion

In this article, we dived into what asynchronous web development in Python looks like - it's advantages and uses. Afterwards, we built a simple reactive aiohttp based app that dynamically displays the current relevant sky coordinates of planets from the Solar System, given the geographic coordinates of the user.

Upon building the application, we've prepped it for deployment on Heroku.