skip to content

What does a PEP 3333 WSGI application callable receive, and what must it return?

level: middleimportance: must knowfreq 50%

answer

  1. One shape, two objects agree on it
  2. Two arguments in, one thing out
  3. A dict of request variables
  4. A callable for status and headers
  5. Iterable of bytes, never str

basics

~20 s

A WSGI application is any callable of two arguments: environ, a dictionary of CGI-style request variables, and start_response, which the application calls with a status string and a list of header pairs. It returns an iterable of bytes.

solid answer

~50 s

PEP 3333 defines a one-way calling convention. The server calls the application object with `environ` — a plain dict holding CGI-style keys such as `REQUEST_METHOD`, `PATH_INFO` and `QUERY_STRING`, request headers under `HTTP_`-prefixed keys, plus server extensions for the request-body stream, an error stream and the URL scheme — and with `start_response`, a callable the application must invoke with a status string like `'200 OK'` and a list of `(name, value)` header tuples **before** any body data is produced. The return value is an *iterable of bytes*: `[body]` in the common case, or a generator when the response is streamed. If that iterable has a `close` method the server must call it, which is the application's cleanup hook. Any callable satisfies the contract — a function, a bound method, or an instance whose class defines `__call__`.

code

python · 16 lines
python
from wsgiref.util import setup_testing_defaults


def app(environ, start_response):
    body = f"{environ['REQUEST_METHOD']} {environ['PATH_INFO']}\n".encode()
    start_response("200 OK", [("Content-Type", "text/plain"),
                              ("Content-Length", str(len(body)))])
    return [body]


environ = {}
setup_testing_defaults(environ)
head = []
chunks = app(environ, lambda status, headers: head.append((status, headers)))
print(head)
print(list(chunks))

go deeper

for a junior

Be ready to say what WSGI is for in one sentence: a shared calling convention so any Python web application runs on any conforming server. Recall the two arguments by name and that the body is bytes.

for a middle

Explain the mechanics: what lives in environ, when start_response must be called relative to producing body data, and why the return value is an iterable rather than a string. Know that returning str is invalid.

for a senior

Show production judgment: streaming with a generator, the close hook for releasing resources, replacing a status via exc_info, and the fact that one application object serves concurrent requests so shared mutable state needs a lock.

for a principal

Own the boundary argument: WSGI fixed the framework-times-server matrix and its stability is why synchronous Python deployment is boring. Be able to say what it deliberately leaves out, and what that omission costs once a service needs streaming or long-lived connections.

### Why the convention exists Before PEP 333 (2003) and its Python 3 revision **PEP 3333** (2010), every Python web framework spoke a different server API, so deploying was a framework-times-server matrix. WSGI collapses that to one calling convention: any conforming server can drive any conforming application, and *middleware* — an object that is an application to the server above it and a server to the application below it — can be stacked between them. Nothing in the contract is a class you subclass or a library you import; it is a shape two objects agree on. ### The two arguments **`environ`** is an ordinary dict, not `os.environ`, though the key names come from CGI. It carries the request line and headers already parsed: `REQUEST_METHOD`, `SCRIPT_NAME` (the part of the path routed to this application), `PATH_INFO` (the rest), `QUERY_STRING`, `CONTENT_TYPE`, `CONTENT_LENGTH`, `SERVER_NAME`, `SERVER_PORT`, `SERVER_PROTOCOL`, and every request header as an upper-cased `HTTP_`-prefixed key with hyphens turned into underscores. Alongside those, the server adds its own extension keys, prefixed to mark them as server-supplied: a readable byte stream for the request body, a writable text stream for error output, the URL scheme, and boolean flags telling the application whether it is running multithreaded, multiprocess, or once per process. Those flags matter: they are how an application learns whether it may keep mutable module-level state. **`start_response(status, response_headers, exc_info=None)`** is how the application hands back the head of the response. `status` is a string like `'404 Not Found'` — code *and* reason phrase. `response_headers` is a list of two-tuples. The call must happen before the first body byte reaches the server, but a common and legal pattern is to call it lazily from inside a generator just before the first `yield`, so an application can still decide the status after doing work. It may be called a second time only with `exc_info` supplied, and only while no body bytes have been written; that is the escape hatch for replacing a half-composed `200` with an error page. ### The return value The application returns an **iterable of bytes** — the plural matters. `return [b'hello']` is a one-chunk response; `yield`ing from a generator streams, letting the server write each chunk as it is produced so a large body never has to be materialised. Returning a bare `bytes` object technically iterates, but it yields integers, so it is a bug, and returning `str` is simply invalid — the application owns the encoding and must do it. If the returned iterable exposes `close`, the server is required to call it when the response finishes or is aborted, which is where a generator's `finally` block releases a cursor, a file handle or a lock. ### What the server owns, and what it does not The server parses the request, manages keep-alive and transfer encoding, decides the concurrency model, and writes the socket. The application owns everything above that. WSGI deliberately says nothing about routing, form parsing, sessions, templating or authentication — those live in frameworks built on top. It also says nothing about `async`; the callable is synchronous by construction, which is the limitation ASGI exists to lift. ### Concurrency and state One application object typically serves every request in the process, and under a threaded server it is called concurrently. All per-request state must live in locals or in `environ`; module-level mutable state is shared and needs a lock. The multithread and multiprocess flags in `environ` exist precisely so an application can detect which regime it is in. ### The mistakes that show up in interviews Returning `str` instead of bytes; forgetting that `start_response` returns a legacy `write` callable rather than the response; calling `start_response` after the first chunk has been yielded; treating `environ` as process environment variables; and assuming the application is instantiated per request when in fact one object handles them all.

  • When may an application call start_response a second time?
    Only when it passes a third `exc_info` argument, and only while no body bytes have reached the server. That lets an application that has already composed a `200` replace it with an error status. If output has already been written the server must re-raise the exception carried in `exc_info` instead of swapping the status, because the head of the response is already on the wire.
  • What is the write callable that start_response returns for?
    PEP 3333 makes `start_response` return a legacy `write` callable that pushes body bytes immediately. It exists only so frameworks ported from older server APIs, which cannot restructure themselves into an iterable, still work. New code should return or yield bytes instead: `write` forces a blocking send and takes away the server's freedom to buffer, chunk or abandon the response.
  • Why does PEP 3333 prefer an iterable over one big bytes object?
    An iterable lets the server pull one chunk at a time, so a large response is never fully materialised and the first bytes reach the client sooner. It also gives the protocol a defined end point: when iteration finishes the server calls `close` if the iterable has one, which is where the application releases a cursor, a file handle or a lock.

saying these in an interview costs you the question

  • Says the application returns a str instead of bytes
  • Thinks start_response returns the response to the server
  • Calls start_response after the first body chunk is yielded
  • Believes WSGI itself defines routing or form parsing
  • Confuses environ with the process environment variables
  • Assumes a fresh application object per request

context