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.
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.
Leandro has 15+ years in IT/development. Working with Python since 2013, he loves building efficient and cost-effective systems.
Expertise
Previously At
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.

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
andstart_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:
Key | Value | Comments |
---|---|---|
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.multithread | False |
True if server is multithreaded |
wsgi.multiprocess | False |
True if server runs multiple processes |
wsgi.run_once | False |
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 tuple
s, 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

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.
Leandro Lima
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.
Expertise
PREVIOUSLY AT