Web
Article

Python Web Applications: The basics of WSGI

By Adam Bard

The basics of WSGI

Beneath Django, Flask, Bottle, and every other Python web framework, lies the Web Server Gateway Interface, or WSGI for short. WSGI is to Python what Servlets are to Java — a common specification for web servers that allows different web servers and application frameworks to interact based on a common API. However, as with most things, the Python version is considerably simpler.

WSGI is defined in PEP 3333, which I encourage you to read as a reference if you want more information after this quick intro.

This article will introduce you to the WSGI spec from an application developer’s perspective, and show you how to work directly with WSGI to create applications (if you so desire).

Your First WSGI App

Here’s the most basic Python web app possible:

def app(environ, start_fn):
    start_fn('200 OK', [('Content-Type', 'text/plain')])
    return ["Hello World!\n"]

That’s it! The whole file. Call it app.py and run it with any WSGI-compatible server and you’ll get a Hello World response with a 200 status. You can use gunicorn for this; just install it via pip (pip install gunicorn) and run it with gunicorn app:app. This command tells gunicorn to get the WSGI callable from the app variable in the app module.

Right now, you should be pretty excited. Just 3 lines for a running application? That must be some sort of record (barring PHP, because mod_php is cheating). I bet you’re just raring to know more.

So what are the essential parts of a WSGI application?

  • A WSGI application is a Python callable, such as a function, a class, or a class instance with a __call__ method
  • The application callable must accept two arguments: the environ, which is a Python dict containing the request data, and start_fn, itself a callable.
  • The application must call the start_fn with two arguments: the status code (as a string), and a list of headers expressed as 2-tuples.
  • The application returns an iterable containing the bytes in the response body in handy, streamable chunks — in this case, a list of strings containing just "Hello, World!". (If app is a class, this can be accomplished in the __iter__ method.)

By way of example, these next two examples are equivalent to the first:

class app(object):

    def __init__(self, environ, start_fn):
        self.environ = environ
        self.start_fn = start_fn

    def __iter__(self):
        self.start_fn('200 OK', [('Content-Type', 'text/plain')])
        yield "Hello World!\n"
class Application(object):
    def __call__(self, environ, start_fn):
        start_fn('200 OK', [('Content-Type', 'text/plain')])
        yield "Hello World!\n"

app = Application()

You might already be thinking of ways that you can use this information, but probably the most relevant one is writing middlewares.

Jazzing It Up

Middlewares are an easy way to extend the functionality of WSGI apps. Since you need only provide a callable, you can wrap it up in other functions however you please.

For example, say we want to examine the contents of environ. We can easily create a middleware to do so, as in this example:

import pprint


def handler(environ, start_fn):
    start_fn('200 OK', [('Content-Type', 'text/plain')])
    return ["Hello World!\n"]


def log_environ(handler):
    def _inner(environ, start_fn):
        pprint.pprint(environ)
        return handler(environ, start_fn)
    return _inner


app = log_environ(handler)

Here, log_environ is a function that returns a function, which pretty-prints the environ argument before deferring to the original callback.

The advantage of writing middlewares this way is that the middleware and the handler don’t have to know or care about each other. You could easily bolt log_environ onto a Flask application, for example, since Flask apps are WSGI apps.

A few other useful middleware ideas:

import pprint


def handle_error(handler):
    def _inner(environ, start_fn):
        try:
            return handler(environ, start_fn)
        except Exception as e:
            print e  # Log error
            start_fn('500 Server Error', [('Content-Type', 'text/plain')])
            return ['500 Server Error']
    return _inner


def wrap_query_params(handler):
    def _inner(environ, start_fn):
        qs = environ.get('QUERY_STRING')
        environ['QUERY_PARAMS'] = urlparse.parse_qs(qs)
        return handler(environ, start_fn)
    return _inner

You can use reduce to apply a bunch of middleware at once if you don’t want to make a big pyramid a the bottom of your file:

# Applied from bottom to top on the way in, then top to bottom on the way out
MIDDLEWARES = [wrap_query_params,
               log_environ,
               handle_error]

app = reduce(lambda h, m: m(h), MIDDLEWARES, handler)

You can also write middleware that modifies the response, by taking advantage of the start_fn argument. Here’s a middleware that reverses the output if the Content-Type header is text/plain:

def reverser(handler):

    # A reverse function
    rev = lambda it: it[::-1]

    def _inner(environ, start_fn):
        do_reverse = []  # Must be a reference type such as a list

        # Override start_fn to check the content type and set a flag
        def start_reverser(status, headers):
            for name, value in headers:
                if (name.lower() == 'content-type'
                        and value.lower() == 'text/plain'):
                    do_reverse.append(True)
                    break

            # Remember to call `start_fn`
            start_fn(status, headers)

        response = handler(environ, start_reverser)

        try:
            if do_reverse:
                return list(rev(map(rev, response)))

            return response
        finally:
            if hasattr(response, 'close'):
                response.close()
    return _inner

It’s a little more tangled thanks to the separation of start_fn and response, but still perfectly workable.

Also note that, to be strictly spec-compliant with WSGI, we must check for a close method on the response and call it if present. Legacy WSGI applications may also return a write function instead of an iterable upon calling handler; if you want your middleware to support older applications, you may need to handle this case.

Once you start playing with raw WSGI a little bit, you start to understand why Python has literally dozens of web frameworks. WSGI makes it pretty simple to build something up starting from scratch. For example, you might be considering the problem of routing:

routes = {
    '/': home_handler,
    '/about': about_handler,
}


class Application(object):
    def __init__(self, routes):
        self.routes = routes

    def not_found(self, environ, start_fn):
        start_fn('404 Not Found', [('Content-Type', 'text/plain')])
        return ['404 Not Found']

    def __call__(self, environ, start_fn):
        handler = self.routes.get(environ.get('PATH_INFO')) or self.not_found
        return handler(environ, start_fn)

Working with WSGI directly can be nice if you enjoy the flexibility of assembling libraries over

  • Template libraries: just drop in any template library you like (e.g. Jinja2, Pystashe) and return the rendered template from your handler!
  • Help your routing with a library like Routes or perhaps Werkzeug’s routing. Actually, take a look at Werkzeug if you want to use an ever-so-slight abstraction over WSGI.
  • Use any database/migration libraries as you would with Flask or similar.

Of course, for non-specialized applications, you’ll probably still want to use a framework just so that edge cases are properly handled and whatnot.

But What About Servers?

There are a bunch of ways to serve WSGI apps. We already talked about Gunicorn, which is a decent option. uWSGI is another great option. Just make sure you set up something like nginx in front of these to serve static assets and you should have a solid starting point.

And that’s all there is to it!

More:
  • Juri

    Hello, I come in peace :-)
    Why using python ( and related frameworks ) to develop web applications, rather than languages ​​optimized for the web and database (eg php)?

  • http://oz123.github.io/ Oz Tiram

    Your example returning status “200” will cause error with standard WSGI servers. The status report should be
    at least 4 characters. Alas, the fix is easy, just change “200” to “200 OK”.

    • Adam Bard

      I have no idea what you’re talking about ¬_¬

  • Adam Bard

    Thanks for pointing that out! I was trying to present the simplest possible WSGI middleware, but it seems “simple” and “spec-compliant” aren’t quite compatible in this case.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Instant Website Review

Use Woorank to analyze and optimize your website to improve your website to improve your ranking!

Run a review to see how your site can improve across 70+ metrics!

Get the latest in Front-end, once a week, for free.