Matt Howlett

Matt Howlett

every blog should have a tagline

Python Web Development From The Bottom Up

2019-10-23

This blog post is a quick overview of web application development in Python with an emphasis on the lower level technologies.

WSGI

As a newcomer, one of the first things you'll undoubtedly come across is the term WSGI (Web Server Gateway Interface).

From the The Hitchhiker's Guide To Python:

The majority of self-hosted Python applications today are hosted with a WSGI server such as Gunicorn, either directly or behind a lightweight web server such as nginx.

The WSGI servers serve the Python applications while the web server handles tasks better suited for it such as static file serving, request routing, DDoS protection, and basic authentication.

There are a number of WSGI servers to choose from (which generally have wider scope than just managing WSGI applications). From my work at Confluent, I know of more than one high profile Silicon Valley company using Gunicorn. It's also easy to use, so seems a good default choice.

What exactly is WSGI?

It's an interface specification (very much like the Servelet interface in Java). It's a very simple standard, but also very flexible. Adam Bard wrote some commentary on it here - go read that, it's good. The official spec is very readable as well.

To cut a short story shorter, your web application is defined by a Python callable (a function, a class, or a class instance with a __call__ method), which you provide to the WSGI server. This is executed once corresponding to each request.

Something that isn't conclusive without a bit of digging are the specifics around the lifetime of this object. Quoting the spec:

The server side invokes a callable object that is provided by the application side. The specifics of how that object is provided are up to the server or gateway. It is assumed that some servers or gateways will require an application's deployer to write a short script to create an instance of the server or gateway, and supply it with the application object. Other servers and gateways may use configuration files or other mechanisms to specify where an application object should be imported from, or otherwise obtained.

I think it's clear from that that the callable is always long lived and reused by the server to service multiple requests. AFAICT, this is always the case in practice.

Typically, a WSGI server will create multiple worker processes and there will be one instance of the application callable in each. Typically, each of these workers can also be configured to be multi-threaded. In that case, a single application instance will still be used to service all requests in a given worker.

Because of Python's global interpreter lock (GIL), increasing the number of threads is an ineffective way to scale your application if it is CPU bound. On the other hand, if your request handlers do a lot of waiting on IO (which is typical), threads are a better choice because there they come with less overhead than processes.

For more information on optimizing gunicorn, check out this blog post.

ASGI

WSGI is a synchronous interface. If a request handler needs to do IO, it needs to block a thread to do so. ASGI was conceived following the addition of asyncio coroutines to the Python language, which provide a much more efficient mechanism for IO heavy workloads than threading. It serves the same purpose to WSGI, but the application is specified as an asyncio-compatible coroutine.

Another difference is that unlike WSGI, ASGI explicitly addresses application lifespan.

ASGI is less prevelant than WSGI, for example it's not yet supported by gunicorn and maintainers of popular web frameworks are apparently just discovering it. It seems to have momentum though, and as Python 2 nears end-of-life seems destined to increase in popularity. The most popular ASGI server today is uvicorn.

Finally, some web frameworks don't require either WSGI or ASGI. From the documentation of the popular asyncio based framework sanic:

Deploying Sanic is very simple using one of three options: the inbuilt webserver, an ASGI webserver, or gunicorn. It is also very common to place Sanic behind a reverse proxy, like nginx.

In this case, integration with gunicorn is via a custom plugin.

Frameworks

The WSGI and ASGI standards are so straightforward it's practical to build simple web applications directly on top of them. However, for all but the most simple tasks, you'll want to leverage the conveniences offered by a higher level framework.

Of the traditional WSGI Frameworks, Flask and Django stand out as two popular choices, but the space is very fragmented and there are many popular frameworks to choose from. Flask is referred to as a microframework, providing only the basics out of the box (notably routing and page templating), delegating other features to extensions. Django is Python's answer to Ruby on Rails and has an opinionated view on everything from the database abstraction layer to form validation.

Given the advantages of asyncio over thread based IO and the rapidly increasing maturity of the asyncio ecosystem, I think choosing a WSGI framework for most new project is probably the wrong choice. Django does plan to add support, but it's a big task and a long way off.

Geek Flare have a recent blog post outlining the current landscape of async web frameworks. Of these, FastAPI seems the most promising to me - I'm going to check it out.