Python web applications have long adhered to the Web Server Gateway Interface (WSGI) standard, which describes how they talk to web servers. WSGI, originally introduced in 2003 and updated in 2010, relies only on features that were available natively in Python as of version 2.2 and were easy to implement. As a result, WSGI enjoyed quick uptake with all the major Python web frameworks and became a cornerstone of web development in Python.
Fast-forward to 2022. Python 2 is obsolete (at long last), and Python now has native syntax for handling asynchronous operations like network calls. WSGI and other standards that assume synchronous behaviors by default can’t take advantage of the performance and efficiency gains of async. That in turn means WSGI can’t effectively handle advanced protocols like WebSocket.
Enter ASGI, the Asynchronous Server Gateway Interface. Like WSGI, ASGI describes a common interface between a Python web application and the web server. Unlike WSGI, ASGI allows multiple, asynchronous events per application. Plus, ASGI supports both sync and async apps. You can migrate your old, synchronous WSGI web apps to ASGI, as well as use ASGI to build new, asynchronous web apps.
How WSGI works
WSGI works by exposing a Python function, typically named
app, to the web server. This function takes two parameters:
environ: A dictionary that contains information about the current request and the environment variables provided by the web server.
start_response: A function that will be used to initiate sending an HTTP response back to the client.
The data returned by the function constitutes the response body.
application function might look like this:
def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'Greetings universe']
If you’re using a WSGI-compatible web framework like Flask, the framework itself will provide an
application function, with all its components automatically wired up.
The downsides of WSGI are twofold. First, WSGI handles only a single request and response at a time, with the assumption that the response will be immediately returned. There’s no way to deal with long-held connections, such as a WebSocket or long-polling HTTP connection.
Second, WSGI is synchronous only. Even if you use a multithreaded connection pool, each connection will block until it returns a response. Many WSGI setups have the ability to handle thread and process pools, but those are constrained by the WSGI interface itself being synchronous.
How ASGI works
ASGI is outwardly similar to WSGI. As with WSGI, you define an
application function object, except it’s an
async function with three parameters instead of two:
scope: A dictionary with information about the current request, akin to
environin WSGI, but with a slightly different naming convention for the details.
asynccallable (function) that lets the application send messages back to the client.
asynccallable that lets the application receive messages from the client.
A simple ASGI
application function might look like this:
async def application(scope, receive, send): await send( 'type': 'http.response.start', 'status': 200, 'headers': [ [b'content-type', b'text/plain'], ], ) await send( 'type': 'http.response.body', 'body': b'Hello, world!', )
Like a WSGI web framework, an ASGI web framework will generate its own
application() function and wire it up as needed.
The most obvious difference with ASGI is that we’re using async metaphors throughout the function. The function itself is
async, and we send the HTTP headers and the response body by way of two separate
await send() commands. This way, the function itself, and its
send commands, don’t block anything; they can be interleaved with invocations of
send from many other connections at once.
We aren’t using
receive in this example, but it too is an
async function. It lets us receive the request body without blocking other operations. Requests and responses can be streamed to or from the server incrementally this way—something we couldn’t do elegantly, or maybe at all, using WSGI.
Using sync and async functions with ASGI
When using ASGI, you’ll want to use
async functions, and async-friendly libraries, as much as possible. It pays to get in the habit of using
async, because the problems with using sync-only code can be significant. Any long-running call to a sync-only function will block the entire call chain, making the benefits of using async all but evaporate.
If you’re stuck using a long-running synchronous call for something, use
asyncio.run_in_executor to farm out the call to a thread or process pool. A thread pool should be used whenever you’re waiting on an external event or a task that isn’t CPU-intensive. A process pool should be used for local tasks that are CPU-intensive.
For instance, if you have a route in your web application that makes a call to a remote website, you should use a thread—or, better yet, use the
aiohttp library, which makes async HTTP requests. If you want to invoke the Pillow image library to resize an image, you probably should use
run_in_executor with a process pool. Although there will be some slight overhead to shuttle data back and forth between processes, using
run_in_executor will not block other events.
ASGI-ready web frameworks
It’s possible to write ASGI web apps “by hand” by implementing the
application() object. But the vast majority of the time it will be simpler (and less headache-inducing) to use an async-native, ASGI-centric Python web framework. Here are some common choices of web framework that play well with ASGI:
- Starlette and FastAPI: These up-and-coming frameworks (FastAPI is built atop Starlette) are both async-first, so it’s no surprise they both support ASGI. If you’re starting a web app from a blank slate, they’re the most modern and cutting-edge of web frameworks for Python.
- Quart: While the staple Python web framework Flask does support ASGI, Flask is not designed from the inside out to take advantage of async metaphors. Quart, from GitLab, uses Flask’s syntax and metaphors, but allows async route handlers.
- Django 3.0 and later: As of Django 3.0, the venerable Django web framework supports ASGI. Support for async code within a Django application, as opposed to just being able to mount Django on an ASGI handler, was added in Django 3.1. For a framework not known for its execution speed, the mere presence of async unlocks greater performance for those who choose to leverage it.
Copyright © 2022 IDG Communications, Inc.