11 WSGI, Flask, Django
HTTP
First, HTTP Review.
WSGI
Python offers a common interface to interact with HTTP known as WSGI.
Web Server Gateway Interface, a common interface allowing a Python program to treat an HTTP request as a call to a Python function, which in turn returns a
- status code
- headers
- body
What we think of as a complete HTTP Response.
Example 01: simplest WSGI app
from typing import Callable, Iterable
from wsgiref.simple_server import make_server
def application(environ: dict[str, str], start_response: Callable[[str, dict[str, str]]]) -> Iterable[bytes]:
status = '200 OK'
headers = [('Content-Type', 'text/plain')]
start_response(status, headers)
return [b'Hello, World!']
server = make_server('localhost', 8000, application)
server.serve_forever()Specification: https://peps.python.org/pep-0333/
Example 02: path routing and URL parameters
from wsgiref.simple_server import make_server
from urllib.parse import parse_qs
def application(environ, start_response):
match environ["PATH_INFO"]:
case "/":
status, body = "200 OK", """
<h1>02wsgi.py</h1>
<form action="/hello" method="GET">
<input name="name" type="text" placeholder+"name" />
<input type="submit">
</form>
""".encode()
case "/hello":
params = parse_qs(environ.get("QUERY_STRING", ""))
name = params.get("name", ["World"])[0]
status, body = "200 OK", f"<h1>Hello, {name}!</h1>".encode()
case _:
status, body = "404 Not Found", b"<h1>404</h1>"
start_response(status, [("Content-Type", "text/html")])
return [body]
server = make_server("localhost", 8000, application)
server.serve_forever()Example 03: Streaming
from wsgiref.simple_server import make_server
import time
def app(environ, start_response):
start_response("200 OK", [("Content-Type", "text/plain")])
while True:
yield f"{time.strftime('%H:%M:%S')}\n".encode()
time.sleep(1)
server = make_server("localhost", 8000, app)
server.serve_forever()Flask
Flask is a fairly thin wrapper around this idea. It offers a handful of convenience classes and methods for common cases:
Example 04: Flask
Our WSGI app converted to Flask:
from flask import Flask, request
app = Flask(__name__)
@app.route('/')
def index():
return '''
<h1>Flask version</h1>
<form action="/hello" method="GET">
<input name="name" type="text" placeholder="name" />
<input type="submit">
</form>
'''
@app.route('/hello')
def hello():
name = request.args.get('name', 'World')
return f'<h1>Hello, {name}!</h1>'
if __name__ == "__main__":
app.run(debug=True)Example 05: Writing a MiniFlask
Flask isn’t particularly complicated, we can replicate the core functionality in a few lines:
from urllib.parse import parse_qs
from wsgiref.simple_server import make_server
class MiniFlask:
def __init__(self):
self.routes = {}
def route(self, path):
"""
app.route(...) returns a decorator that adds the route under
the assigned path
"""
def decorator(fn):
self.routes[path] = fn
return fn
return decorator
def __call__(self, environ, start_response):
"""
Notice the signature matches the WSGI interface.
__call__ invoked when we run `app = MiniFlask(); app(...)`
It allows us to treat a class instance like a function
which we use here so that we can store `self.routes` on the
app "function" call for WSGI.
"""
handler = self.routes.get(environ["PATH_INFO"])
if handler:
# will extract querystring and parse into our function
# as 'args'
#
# Flask handles this with the global `request` object
# set before the function is called.
#
# Django (and others) do something similar to what
# we do here, but with more than the querystring
# gathered into a *Request* object.
args = parse_qs(environ.get("QUERY_STRING", ""))
status, body = "200 OK", handler(args).encode()
else:
status, body = "404 Not Found", b"<h1>404</h1>"
start_response(status, [("Content-Type", "text/html")])
return [body]
app = MiniFlask()
@app.route("/")
def index(args):
return """
<form action="/hello" method="GET">
<input name="name" type="text" placeholder="name" />
<input type="submit">
</form>
"""
@app.route("/hello")
def hello(args):
name = args.get("name", ["World"])[0]
return f"<h1>Hello, {name}!</h1>"
# app is callable via __call__
server = make_server("localhost", 8000, app)
server.serve_forever()Example 06: A more “full-featured” Flask app
from flask import Flask, request, jsonify, render_template
import random
app = Flask(__name__)
notes = []
COLORS = ["khaki", "lightblue", "lightgreen", "lightsalmon", "plum"]
@app.route("/")
def index():
return render_template("notes.html")
@app.route("/api/notes", methods=["GET"])
def get_notes():
return jsonify(notes)
@app.route("/api/notes", methods=["POST"])
def add_note():
note = request.json.get("text")
notes.append({"text": note, "color": random.choice(COLORS)})
return jsonify(notes), 201
if __name__ == "__main__":
app.run(debug=True)Middleware
We often find that there are things we want to do to every request:
- add/verify security headers
- augment/convert responses
- log requests, trace performance, etc.
The simplicity of the WSGI interface:
def application(environ: dict[str, str], start_response: Callable[[str, dict[str, str]]]) -> Iterable[bytes]:provides us a single point where we have the opportunity to capture/modify incoming requests (environ), or to intercept outgoing responses.
def app(environ, start_response):
environ = preprocess(environ) # preprocess incoming request
view_handler = pick_handler(environ) # dispatch based on URL
status, headers, raw_response = view_handler(environ) # call view
# postprocess response
new_response, new_status, new_headers = postprocess(
status, headers, raw_response, environ
)
start_response(new_status, new_headers) # reply to web server
return new_responseWe’ve seen this pattern before when discussing decorators.
In practice when we’re wrapping every request, we can decorate the app itself and not the individual views.
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]Django
Django is a more full-featured web application framework.
It encourages you to segment your logic into:
models.py- handle your relationship to your database and other storageviews.py- functions to handle individual URL routesurls.py- mapping of URLs to views (separation allows views to more easily serve different routes)
It also has a built-in template engine, allowing designers to write HTML that accesses selected functionality exposed by the backend.
In addition to these core features replicating Jinja functionality it provides:
- middlewares
- signals - ways for models/views to send messages to other objects when certain things occur (user added -> auto create profile; new message -> trigger email in queue)
- built-in applications, pre-written/bundled models/views/templates used for common tasks (auth, database-backed CRUD, etc.)
- a comprehensive test framework that provides mocks
Example Django App
uv add django
uv run django-admin startproject notes
cd notes
uv run ./manage.py startapp core
notes/
manage.py
notes/
settings.py
urls.py
core/
views.py
urls.py
templates/
index.html
views.py
import json
import random
from django.http import JsonResponse
from django.shortcuts import render
from .models import Note
COLORS = ["khaki", "lightblue", "lightgreen", "lightsalmon", "plum"]
notes = []
def index(request):
return render(request, "index.html")
def api_notes(request):
if request.method == "GET":
return JsonResponse(notes, safe=False)
elif request.method == "POST":
data = json.loads(request.body)
notes.append({"text": data["text"], "color": random.choice(COLORS)})
return JsonResponse(notes, safe=False, status=201)
def api_notes_persist(request):
if request.method == "GET":
notes = list(Note.objects.values("text", "color"))
return JsonResponse(notes, safe=False)
elif request.method == "POST":
data = json.loads(request.body)
Note.objects.create(text=data["text"], color=random.choice(COLORS))
notes = list(Note.objects.values("text", "color"))
return JsonResponse(notes, safe=False, status=201)With Models
models.py
from django.db import models
class Note(models.Model):
text = models.TextField()
color = models.CharField(max_length=20)views.py
import json
import random
from django.http import JsonResponse
from django.shortcuts import render
from .models import Note
COLORS = ["khaki", "lightblue", "lightgreen", "lightsalmon", "plum"]
notes = []
def index(request):
return render(request, "index.html")
def api_notes(request):
if request.method == "GET":
return JsonResponse(notes, safe=False)
elif request.method == "POST":
data = json.loads(request.body)
notes.append({"text": data["text"], "color": random.choice(COLORS)})
return JsonResponse(notes, safe=False, status=201)
def api_notes_persist(request):
if request.method == "GET":
notes = list(Note.objects.values("text", "color"))
return JsonResponse(notes, safe=False)
elif request.method == "POST":
data = json.loads(request.body)
Note.objects.create(text=data["text"], color=random.choice(COLORS))
notes = list(Note.objects.values("text", "color"))
return JsonResponse(notes, safe=False, status=201)Making Choices
Flask
- Simple request/response mapping.
- Can be a single file, or scale to several (Flask blueprints)
- Good ecosystem for common tasks, can
- Lets you make your own choices for models/templates/etc.
- SQLAlchemy
- Jinja2
- Built in debugger, aimed at getting productive quickly!
Small API, can learn in an afternoon: https://flask.palletsprojects.com/en/stable/
Django
django.contrib.gisdjango.contrib.admindjango.contrib.auth- Better security posture out of the box.
- MVT enforces more separation of code.
- Huge “reusable apps” ecosystem.
- Developing first-class
asyncsupport, still somewhat in-progress.
Great documentation, but a lot to learn: https://www.djangoproject.com/start/