Back-end9 minute read

WSGI: The Server-Application Interface for Python

Nowadays, almost all Python frameworks use WSGI as a means, if not the only means, to communicate with their web servers. This is how Django, Flask, and many other popular frameworks do it.

This article intends to provide readers with a glimpse into how WSGI works and allow them to build a simple WSGI application or server.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

Nowadays, almost all Python frameworks use WSGI as a means, if not the only means, to communicate with their web servers. This is how Django, Flask, and many other popular frameworks do it.

This article intends to provide readers with a glimpse into how WSGI works and allow them to build a simple WSGI application or server.


Toptalauthors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.
Leandro Lima
Verified Expert in Engineering

Leandro has 15+ years in IT/development. Working with Python since 2013, he loves building efficient and cost-effective systems.

PREVIOUSLY AT

Embraer
Share

In 1993, the web was still in its infancy, with about 14 million users and 100 websites. Pages were static but there was already a need to produce dynamic content, such as up-to-date news and data. Responding to this, Rob McCool and other contributors implemented the Common Gateway Interface (CGI) in the National Center for Supercomputing Applications (NCSA) HTTPd web server (the forerunner of Apache). This was the first web server that could serve content generated by a separate application.

Since then, the number of users on the Internet has exploded, and dynamic websites have become ubiquitous. When first learning a new language or even first learning to code, developers, soon enough, want to know about how to hook their code into the web.

Python on the Web and the Rise of WSGI

Since the creation of CGI, much has changed. The CGI approach became impractical, as it required the creation of a new process at each request, wasting memory and CPU. Some other low-level approaches emerged, like FastCGI](http://www.fastcgi.com/) (1996) and mod_python (2000), providing different interfaces between Python web frameworks and the web server. As different approaches proliferated, the developer’s choice of framework ended up restricting the choices of web servers and vice versa.

To address this problem, in 2003 Phillip J. Eby proposed PEP-0333, the Python Web Server Gateway Interface (WSGI). The idea was to provide a high-level, universal interface between Python applications and web servers.

In 2003, PEP-3333 updated the WSGI interface to add Python 3 support. Nowadays, almost all Python frameworks use WSGI as a means, if not the only means, to communicate with their web servers. This is how Django, Flask and many other popular frameworks do it.

This article intends to provide the reader with a glimpse into how WSGI works, and allow the reader to build a simple WSGI application or server. It is not meant to be exhaustive, though, and developers intending to implement production-ready servers or applications should take a more thorough look into the WSGI specification.

The Python WSGI Interface

WSGI specifies simple rules that the server and application must conform to. Let’s start by reviewing this overall pattern.

The Python WSGI server-application interface.

Application Interface

In Python 3.5, the application interfaces goes like this:

def application(environ, start_response):
    body = b'Hello world!\n'
    status = '200 OK'
    headers = [('Content-type', 'text/plain')]
    start_response(status, headers)
    return [body]

In Python 2.7, this interface wouldn’t be much different; the only change would be that the body is represented by a str object, instead of a bytes one.

Though we’ve used a function in this case, any callable will do. The rules for the application object here are:

  • Must be a callable with environ and start_response parameters.
  • Must call the start_response callback before sending the body.
  • Must return an iterable with pieces of the document body.

Another example of an object that satisfies these rules and would produce the same effect is:

class Application:
    def __init__(self, environ, start_response):
        self.environ = environ
        self.start_response = start_response

    def __iter__(self):
        body = b'Hello world!\n'
        status = '200 OK'
        headers = [('Content-type', 'text/plain')]
        self.start_response(status, headers)
        yield body

Server Interface

A WSGI server might interface with this application like this::

def write(chunk):
    '''Write data back to client'''
    ...

def send_status(status):
   '''Send HTTP status code'''
   ...

def send_headers(headers):
    '''Send HTTP headers'''
    ...

def start_response(status, headers):
    '''WSGI start_response callable'''
    send_status(status)
    send_headers(headers)
    return write

# Make request to application
response = application(environ, start_response)
try:
    for chunk in response:
        write(chunk)
finally:
    if hasattr(response, 'close'):
        response.close()

As you may have noticed, the start_response callable returned a write callable that the application may use to send data back to the client, but that was not used by our application code example. This write interface is deprecated, and we can ignore it for now. It will be briefly discussed later in the article.

Another peculiarity of the server’s responsibilities is to call the optional close method on the response iterator, if it exists. As pointed out in Graham Dumpleton’s article here, it is an often-overlooked feature of WSGI. Calling this method, if it exists, allows the application to release any resources that it may still hold.

The Application Callable’s environ Argument

The environ parameter should be a dictionary object. It is used to pass request and server information to the application, much in the same way CGI does. In fact, all CGI environment variables are valid in WSGI and the server should pass all that apply to the application.

While there are many optional keys that can be passed, several are mandatory. Taking as an example the following GET request:

$ curl 'http://localhost:8000/auth?user=obiwan&token=123'

These are the keys that the server must provide, and the values they would take:

KeyValueComments
REQUEST_METHOD"GET"
SCRIPT_NAME""server setup dependent
PATH_INFO"/auth"
QUERY_STRING"token=123"
CONTENT_TYPE""
CONTENT_LENGTH""
SERVER_NAME"127.0.0.1"server setup dependent
SERVER_PORT"8000"
SERVER_PROTOCOL"HTTP/1.1"
HTTP_(...) Client supplied HTTP headers
wsgi.version(1, 0)tuple with WSGI version
wsgi.url_scheme"http"
wsgi.input File-like object
wsgi.errors File-like object
wsgi.multithreadFalse True if server is multithreaded
wsgi.multiprocessFalse True if server runs multiple processes
wsgi.run_onceFalse True if the server expects this script to run only once (e.g.: in a CGI environment)

The exception to this rule is that if one of these keys were to be empty (like CONTENT_TYPE in the above table), then they can be omitted from the dictionary, and it will be assumed they correspond to the empty string.

wsgi.input and wsgi.errors

Most environ keys are straightforward, but two of them deserve a little more clarification: wsgi.input, which must contain a stream with the request body from the client, and wsgi.errors, where the application reports any errors it encounters. Errors sent from the application to wsgi.errors typically would be sent to the server error log.

These two keys must contain file-like objects; that is, objects that provide interfaces to be read or written to as streams, just like the object we get when we open a file or a socket in Python. This may seem tricky at first, but fortunately, Python gives us good tools to handle this.

First, what kind of streams are we talking about? As per WSGI definition, wsgi.input and wsgi.errors must handle bytes objects in Python 3 and str objects in Python 2. In either case, if we’d like to use an in-memory buffer to pass or get data through the WSGI interface, we can use the class io.BytesIO.

As an example, if we are writing a WSGI server, we could provide the request body to the application like this:

  • For Python 2.7
import io
...
request_data = 'some request body'
environ['wsgi.input'] = io.BytesIO(request_data)
  • For Python 3.5
import io
...
request_data = 'some request body'.encode('utf-8') # bytes object
environ['wsgi.input'] = io.BytesIO(request_data)

On the application side, if we wanted to turn a stream input we’ve received into a string, we’d want to write something like this:

  • For Python 2.7
readstr = environ['wsgi.input'].read() # returns str object
  • For Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object
readstr = readbytes.decode('utf-8')      # returns str object

The wsgi.errors stream should be used to report application errors to the server, and lines should be ended by a \n. The web server should take care of converting to a different line ending according to the system.

The Application Callable’s start_response Argument

The start_response argument must be a callable with two required arguments, namely status and headers, and one optional argument, exc_info. It must be called by the application before any part of the body is sent back to the web server.

In the first application example at the beginning of this article, we’ve returned the body of the response as a list, and thus, we have no control over when the list will be iterated over. Because of this, we had to call start_response before returning the list.

In the second one, we’ve called start_response just before yielding the first (and, in this case, only) piece of the response body. Either way is valid within WSGI specification.

From the web server side, the calling of start_response shouldn’t actually send the headers to the client, but delay it until the there is at least one non-empty bytestring in the response body to send back to the client. This architecture allows for errors to be correctly reported until the very last possible moment of the application’s execution.

The status Argument of start_response

The status argument passed to the start_response callback must be a string consisting of an HTTP status code and description, separated by a single space. Valid examples are: '200 OK', or '404 Not Found'.

The headers Argument of start_response

The headers argument passed to the start_response callback must be a Python list of tuples, with each tuple composed as (header_name, header_value). Both the name and value of each header must be strings (regardless of Python version). This is a rare example in which type matters, as this is indeed required by the WSGI specification.

Here is a valid example of what a header argument may look like:

response_body = json.dumps(data).encode('utf-8')

headers = [('Content-Type', 'application/json'),
           ('Content-Length', str(len(response_body))]

HTTP headers are case-insensitive, and if we are writing a WSGI compliant web server, that is something to take note of when checking these headers. Also, the list of headers provided by the application isn’t supposed to be exhaustive. It is the server’s responsibility to ensure that all required HTTP headers exist before sending the response back to the client, filling in any headers not provided by the application.

The exc_info Argument of start_response

The start_response callback should support a third argument exc_info, used for error handling. The correct usage and implementation of this argument is of utmost importance for production web servers and applications, but is outside the scope of this article.

Further information on it can be obtained in the WSGI specification, here.

The start_response Return Value – The write Callback

For backward compatibility purposes, web servers implementing WSGI should return a write callable. This callback should allow the application to write body response data directly back to the client, instead of yielding it to the server via an iterator.

Despite its presence, this is a deprecated interface and new applications should refrain from using it.

Generating the Response Body

Applications implementing WSGI should generate the response body by returning an iterable object. For most applications, the response body isn’t very large and fits easily within the server’s memory. In that case, the most efficient way of sending it is all at once, with a one-element iterable. In special cases, where loading the whole body into memory is unfeasible, the application may return it part-by-part through this iterable interface.

There is only a small difference here between Python 2’s and Python 3’s WSGI: in Python 3, the response body is represented by bytes objects; in Python 2, the correct type for this is str.

Converting UTF-8 strings into bytes or str is an easy task:

  • Python 3.5:
body = 'unicode stuff'.encode('utf-8')
  • Python 2.7:
body = u'unicode stuff'.encode('utf-8')

If you’d like to learn more about Python 2’s unicode and bytestring handling, there’s a nice tutorial on YouTube.

Web servers implementing WSGI should also support the write callback for backwards compatibility, as described above.

Testing Your Application Without a Web Server

With an understanding of this simple interface, we can easily create scripts to test our applications without actually needing to start up a server.

Take this small script, for example:

from io import BytesIO

def get(app, path = '/', query = ''):
    response_status = []
    response_headers = []

    def start_response(status, headers):
        status = status.split(' ', 1)
        response_status.append((int(status[0]), status[1]))
        response_headers.append(dict(headers))

    environ = {
        'HTTP_ACCEPT': '*/*',
        'HTTP_HOST': '127.0.0.1:8000',
        'HTTP_USER_AGENT': 'TestAgent/1.0',
        'PATH_INFO': path,
        'QUERY_STRING': query,
        'REQUEST_METHOD': 'GET',
        'SERVER_NAME': '127.0.0.1',
        'SERVER_PORT': '8000',
        'SERVER_PROTOCOL': 'HTTP/1.1',
        'SERVER_SOFTWARE': 'TestServer/1.0',
        'wsgi.errors': BytesIO(b''),
        'wsgi.input': BytesIO(b''),
        'wsgi.multiprocess': False,
        'wsgi.multithread': False,
        'wsgi.run_once': False,
        'wsgi.url_scheme': 'http',
        'wsgi.version': (1, 0),
    }

    response_body = app(environ, start_response)
    merged_body = ''.join((x.decode('utf-8') for x in response_body))

    if hasattr(response_body, 'close'):
        response_body.close()

    return {'status': response_status[0],
            'headers': response_headers[0],
            'body': merged_body}

In this way, we might, for example, initialize some test data and mock modules into our app, and make GET calls in order to test if it responds accordingly. We can see that it isn’t an actual web server, but interfaces with our app in a comparable way by providing the application with a start_response callback and a dictionary with our environment variables. At the end of the request, it consumes the response body iterator and returns a string with all of its content. Similar methods (or a general one) can be created for different types of HTTP requests.

Wrap-Up

WSGI is a critical part of almost any Python web framework.

In this article, we have not approached how WSGI deals with file uploads, as this could be considered a more “advanced” feature, not suitable for an introductory article. If you’d like to know more about it, take a look a the PEP-3333 section referring to file handling.

I hope that this article is useful in helping create a better understanding of how Python talks to web servers, and allows developers to use this interface in interesting and creative ways.

Acknowledgments

I’d like to thank my editor Nick McCrea for helping me with this article. Due to his work, the original text became much clearer and several errors did not go uncorrected.

Hire a Toptal expert on this topic.
Hire Now
Leandro Lima

Leandro Lima

Verified Expert in Engineering

São José dos Campos - State of São Paulo, Brazil

Member since November 19, 2015

About the author

Leandro has 15+ years in IT/development. Working with Python since 2013, he loves building efficient and cost-effective systems.

authors are vetted experts in their fields and write on topics in which they have demonstrated experience. All of our content is peer reviewed and validated by Toptal experts in the same field.

PREVIOUSLY AT

Embraer

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

World-class articles, delivered weekly.

Subscription implies consent to our privacy policy

Join the Toptal® community.