Why Build a Framework? WSGI & the Real Python Web Server Interface

Building a Web Framework

Chapter 1 ยท Why Build a Framework? WSGI & the Real Python Web Server Interface

Web Framework Internals studied how five real frameworks each answer four real questions — routing, templating, data access, middleware — by comparing their real syntax and quoting their real documentation. This course answers the identical four questions a genuinely different way: by building a real, small, working Python web framework from scratch, one capability at a time, ending with a real application running on top of it. Before any of that, though, there's a more basic real question to answer first — how does a Python function even become "a web application" that a real browser can talk to at all?

Scope note — read this before anything else
This course builds a real, working synchronous framework, on top of WSGI — not the newer, async ASGI standard FastAPI (covered in Web Framework Internals) is built on. That's a deliberate choice, explained in full later in this chapter, not a limitation being quietly worked around. Every claim in every chapter of this course is backed by real, hand-run code and, where relevant, real quotes from official specifications — the same discipline this site's own compiler1/compiler2, browserengine1/browserengine2, dbengine1/dbengine2, and oskernel1/oskernel2 projects already used.

The Pipeline This Course Builds

CapabilityWhat it doesBuilt in
RoutingA real path-matching engine with dynamic parameters, mirroring Web Framework Internals' own Chapter 2Chapter 2
Request/response objectsA real, ergonomic wrapper around the raw WSGI interface this chapter buildsChapter 3
TemplatingA real template engine — parsing, compiling, rendering — mirroring Chapter 4Chapter 4
MiddlewareA real onion-model pipeline, mirroring Chapter 8Chapter 5
Sessions & cookiesReal state carried across genuinely stateless HTTP requestsChapter 6
A minimal ORMMapping real Python objects to real SQL, mirroring Chapter 6Chapter 7
Error handling & dev serverA real development server with live reloadChapter 8
Static files & a CLIServing real files directly, and a real command-line tool for the framework itselfChapter 9

The Real Interface Every Synchronous Python Framework Sits On

A Python program doesn't talk to the internet directly — a real web server (Apache, nginx, Gunicorn) handles the actual TCP connections and HTTP parsing, then needs some real, standardized way to hand a parsed request off to Python application code and get a response back. That standard is WSGI (the Web Server Gateway Interface, defined in PEP 3333) — and it's genuinely simple: a WSGI application is just a callable taking two arguments. PEP 3333's own real, minimal example:

def simple_app(environ, start_response): """Simplest possible application object""" status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return [b"Hello world!\n"]

This isn't a sketch — it's real, runnable code. Python's own standard library ships a genuine, working WSGI server (wsgiref) specifically for testing code exactly like this:

from wsgiref.simple_server import make_server import urllib.request server = make_server('127.0.0.1', 8734, simple_app) # ... serve_forever() on a thread ... resp = urllib.request.urlopen('http://127.0.0.1:8734/') print(resp.status, resp.read(), resp.headers.get('Content-type')) # 200 b'Hello world!\n' text/plain
Verified directly — a real HTTP request, a real HTTP response
That's a genuine HTTP client (urllib.request) talking to a genuine, real HTTP server, produced entirely by a 6-line function following one, real, standardized interface. Every real Python web framework — Django, Flask, every one of Web Framework Internals' own routers built in Chapter 2 — ultimately reduces to something shaped like simple_app above, however many layers of routing, templating, and middleware sit in front of it.

The Real environ Dict: What a Server Actually Hands You

environ is a real, plain Python dict — per PEP 3333's own specification, a set of CGI-style variables describing the incoming request. Capturing it from a real request shows exactly what's really in there:

def inspecting_app(environ, start_response): captured_environ.update(environ) start_response('200 OK', [('Content-type', 'text/plain')]) return [b"ok"] # a real request to /users/42?active=true for key in ['REQUEST_METHOD', 'PATH_INFO', 'QUERY_STRING', 'SERVER_PROTOCOL', 'wsgi.url_scheme']: print(key, '=', captured_environ.get(key)) # REQUEST_METHOD = GET # PATH_INFO = /users/42 # QUERY_STRING = active=true # SERVER_PROTOCOL = HTTP/1.1 # wsgi.url_scheme = http
This is the raw material every later chapter builds on
PATH_INFO and QUERY_STRING, verified here as real, plain strings sitting in a real dict, are exactly what Chapter 2's own router will parse to match a route, and exactly what a real request object (Chapter 3) will wrap into something more ergonomic than reading dict keys by hand. Nothing about routing or request objects is magic — it's all reachable, real data already sitting in environ the moment a request arrives.

start_response: A Callback, Not a Return Value

Web Framework Internals verified Rack's own real interface — a middleware and a full application both implement call(env), returning a real [status, headers, body] triplet. WSGI takes a genuinely different real approach: start_response is a callback, called from inside the application, before the body is returned. Getting the order wrong is a real, documented contract violation — and Python's own reference implementation doesn't fail silently:

def broken_app(environ, start_response): # never calls start_response at all return [b"this body is returned, but no status/headers were ever sent"] # real client-side result: # HTTPError: HTTP Error 500: Internal Server Error # # real server-side traceback: # AssertionError: write() before start_response()
Verified directly — a real, documented contract, actively enforced
wsgiref doesn't just document that start_response must be called first — it checks, at runtime, and raises a real AssertionError the instant the rule is broken, converting it into a genuine HTTP 500 rather than sending a malformed response or hanging. The identical real discipline applies to the body itself: returning a plain Python str instead of a list of real bytes objects — a genuinely easy mistake — is caught the same way, verified directly: AssertionError: write() argument must be a bytes instance. PEP 3333's own real requirement, "must return an iterable yielding zero or more bytestrings," isn't just a suggestion in the spec's own prose — it's an actively-checked runtime contract in Python's own reference server.

Real Frameworks Sit on the Identical Interface

Django's own real, default deployment file, wsgi.py, is a genuine, direct instance of exactly this pattern:

# myproject/wsgi.py -- Django's own real, generated file import os from django.core.wsgi import get_wsgi_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') application = get_wsgi_application()

application, the module-level name a real WSGI server looks for, is a genuine, callable WSGI application — underneath Django's own real router, middleware stack, and ORM (all covered across Web Framework Internals' own Chapters 2–7), it still reduces to the identical (environ, start_response) shape simple_app used at the very top of this chapter.

WSGI's Real Limits, and Why This Course Still Builds on It

WSGI's own real successor, ASGI, exists for a real, documented reason — per ASGI's own official documentation, it's "a spiritual successor to WSGI, intended to provide a standard interface between async-capable Python web servers, frameworks, and applications." The same real documentation names the specific real gap directly: "WSGI applications are a single, synchronous callable that takes a request and returns a response; this doesn't allow for long-lived connections, like you get with long-poll HTTP or WebSocket connections."

A deliberate scope choice, not an unnoticed gap
FastAPI, covered throughout Web Framework Internals, is built on ASGI specifically because it needs exactly what WSGI's own real, single-callable model can't provide. This course builds on WSGI anyway — deliberately — because a real, complete, synchronous framework is a genuinely more tractable real project to build chapter by chapter, and every one of this course's own four core capabilities (routing, templating, data access, middleware) is fully expressible on top of it. Real async support, real WebSockets, and real long-lived connections are named here as honestly out of scope, not silently ignored.

Where This Course Is Headed

Chapter 2 builds a real routing engine on top of the environ dict verified in this chapter — real path matching and dynamic parameters, following the exact same real approach Web Framework Internals' own Chapter 2 built and verified.

Hands-On Exercises

Exercise 1

Write a real WSGI application that reads PATH_INFO from environ and echoes it back in the response body (e.g. "You requested: /users/7"), and verify it against three genuinely different real paths using urllib.request against a real wsgiref server.

๐Ÿ“„ View solution
Exercise 2

Write a real WSGI application that calls start_response correctly but returns a plain Python str instead of a list of bytes, run it against a real wsgiref server, and report the exact real exception raised server-side and the real HTTP status the client receives.

๐Ÿ“„ View solution
Exercise 3

Using this chapter's own real start_response callback model and Web Framework Internals' own quoted Rack call(env) -> [status, headers, body] return-triplet model, explain in your own words which of the two interfaces makes a "forgot to call start_response" bug easier to catch automatically, and why.

๐Ÿ“„ View solution

Chapter 1 Quick Reference

  • WSGI โ€” the real, standardized interface (PEP 3333) between a Python web server and Python application code: a callable taking (environ, start_response)
  • environ โ€” a real, plain dict of request data; verified real keys include REQUEST_METHOD, PATH_INFO, QUERY_STRING, SERVER_PROTOCOL
  • start_response โ€” a real callback, not a return value; verified to raise a real AssertionError if called too late or never called at all
  • The body โ€” must be real bytes, not str; verified as an actively-enforced runtime contract, not just documentation
  • Real frameworks confirm this โ€” Django's own real wsgi.py/get_wsgi_application() reduces to the identical callable shape
  • WSGI vs. ASGI โ€” WSGI is a single, synchronous callable; ASGI is its real, documented async successor, needed for WebSockets/long-lived connections โ€” this course deliberately stays on WSGI
  • Next chapter: A routing engine from scratch โ€” path matching & dynamic parameters