Faragiri

Learn Python on the web Python

Fifteen lines with no dependencies that answer a real request, the four things a framework adds on top, and the one that decides between Django and the rest.

4Sections
3Min read
4Depth

Fifteen lines, one GET

What happens between the path and the bytes

Every stop here is a line in the example, and a framework replaces four of the six with something you configure rather than write.

  1. The server builds environPATH_INFO, QUERY_STRING and about thirty other keys
  2. Your function is calledOne argument in, and a callback to answer with
  3. The path is matchedAn if here, a routing table in a framework
  4. The query is parsedparse_qs, and every value arrives as a list
  5. start_response is called200 OK, and the headers, before any body exists
  6. Bytes are returnedAn iterable, so a large response need not be built whole

Nothing on this rail touches a database, a template or a session, and none of those are part of the contract. That is why the choice of framework is really a choice about the fifth thing rather than about these six.

Strip a Python web application to nothing and it is a function taking a
dictionary and returning bytes. This one has no imports beyond the standard
library and answers two requests correctly:

  • GET /stock?code=PL-14 gives 200 OK,
    {'Content-Type': 'application/json', 'Content-Length': '32'} and a
    body of {"code": "PL-14", "inStock": 3}
  • GET /nope gives 404 Not Found and
    no such path

The function receives environ, a dictionary holding
PATH_INFO and QUERY_STRING among about thirty other
keys, and calls start_response(status, headers) before returning an
iterable of bytes. That contract is WSGI, it has not changed since 2003, and
every synchronous Python framework you have used is a program that eventually
calls something with this shape.

The four things a framework actually adds

Routing, so /stock/PL-14 reaches a function
with PL-14 as an argument rather than a string you slice by hand.
Parsing, so a form body or a JSON body arrives as a dict with
the content type already checked. Templating or serialising, so
the response is built rather than concatenated, which is also where the escaping
that stops a cross-site scripting bug lives. And the error path,
so an exception becomes a 500 with a traceback in the log rather than a
connection that closes.

Everything else a framework offers is optional and most of it is the fifth
thing: an object relational mapper, an admin, a migration system, users and
sessions. That is the real division, because it is the one that decides which
framework, rather than a preference about syntax.

Choosing, on the one axis that matters

Django brings the database layer, the admin and the auth system as one
decision, so a project that needs users, a schema that will change, and a screen
for staff to edit rows starts three weeks ahead. Flask and FastAPI bring the
first four things and leave the fifth to you, which is right when the data lives
somewhere else already or when the service answers other programs rather than
people.

FastAPI's own addition is that the function signature is the specification:
annotate the parameters and it validates the request and generates the API
documentation from the same lines. That is genuinely new work rather than a
different spelling, and it is why the choice between it and Flask is not
arbitrary. What either of them is talking to underneath is
the layer between your
objects and the tables
.

What to run in 2026

ASGI is the interface to build on now, free and the standard, and it replaced
WSGI for anything holding a connection open: WSGI's contract has one request in
and one response out with nowhere for a websocket or a long poll to live. The
fifteen lines above are WSGI because it is the smaller thing to read, and the
ASGI version is the same shape with async and a receive channel.

uvicorn is what runs it, free and open source, and as of 2026 it is the usual
answer for Django, FastAPI and anything else ASGI. It replaced running a
framework's own development server in production, which every framework tells
you not to do on the page you skipped. The thing to understand rather than
memorise is that the server and the framework are separate programs, which is
why the traceback in the log sometimes belongs to one and sometimes to the
other.

Where this stalls people

The stall is a framework learned as a set of decorators, so its magic is
opaque and every unusual requirement becomes a search for the incantation.
Nothing above is magic: a request is a dictionary, a response is a status, some
headers and bytes, and a route is a lookup from a path to a function. A person
holding that model can read the framework's own source when the documentation
runs out, which is what the unusual requirement always needs.

The exercise worth doing once: write the fifteen lines, then add routing to
them, and stop. Twenty minutes, and every decorator afterwards is recognisable
as a thing you already built. The lifecycle those bytes travel through on the
way to a browser is
request in, response
out, and everything between
, the environment it has to run inside is
pip, venv, and
keeping projects apart
, and the language underneath is
readable, everywhere, and the usual
first language
.

Alongside this

Above this