Python Web Frameworks Compared: What Is Inside Seven Repos

Raghav ChamadiyaUpdated 10 min read
On this page

Litestar (8.5) and FastAPI (8.4) score highest of seven Python web frameworks, Django scores lowest (6.4), and what each repository ships inside it explains part of that order. Django is the only one of the seven that carries an ORM, a migration system and an admin in the same repo. We read the request path of each framework in its source and scored each repo with repowise.

The seven are FastAPI, Django, Django REST Framework (DRF), Flask, Litestar, Robyn and Sanic. Each repo below links to its public repowise page, and each code link points to the exact commit we indexed.

The numbers first

Health: repowise's health score (1 to 10) rates how likely a file is to cause bugs and how hard it is to change, using static checks plus git history; the repo score is the average across files, weighted by lines of code. Higher is better. Maintainability is scored separately on the same scale.

RepoHealthMaintainabilityFilesLines of codeHotspotsSnapshot
litestar-org/litestar8.59.11,16781,55926 Oct 2026
fastapi/fastapi8.48.42,927101,391106 Oct 2026
sparckles/robyn7.48.236136,83959 Aug 2026
sanic-org/sanic7.2not scored47035,5831031 May 2026
pallets/flask7.07.712814,389013 Aug 2026
encode/django-rest-framework6.98.725046,099022 Jul 2026
django/django6.47.43,010435,0864229 Jul 2026

Scroll the table sideways to see every column.

A hotspot is a file that changes a lot and is also complex, the place where bugs tend to land. "Lines of code" counts non-blank, non-comment lines across the whole repo, including tests and docs examples.

The same three jobs

Every web framework does three things on every request:

  1. Routing: find the handler for this URL and HTTP method.
  2. Input: turn the raw request into the arguments the handler wants, and reject bad input.
  3. Output: turn whatever the handler returned into an HTTP response.

The frameworks differ in where they put those jobs and in how much each one does for you in step 2.

Each repo's own architecture page has the full module map: FastAPI, Django, DRF, Flask, Litestar, Robyn, Sanic.

Django: a framework and everything around it

Django's request path is easy to follow once you find it. BaseHandler._get_response calls resolve_request, which asks the URL resolver for a match, runs any view middleware, wraps the view in a database transaction if you asked for that, and calls it. If the view is async, Django runs it through async_to_sync.

Routing is URLResolver.resolve: it loops over your urlpatterns in order and returns the first one that matches. That design is simple and predictable: order matters, and you can read the code in a few minutes. Input handling for plain Django views is mostly your job, or the forms module's.

Django's low score comes from what is in the repo. 2,018 of Django's 3,010 indexed files are under tests/. The django/ package itself is 931 files and includes the ORM, the migration framework, the admin, auth, sessions, caching, mail, templates and i18n. The longest function repowise found, create_forward_many_to_many_manager, is 640 lines because it is a factory that builds a whole manager class inside itself. The most imported file, django/db/__init__.py, is imported 777 times. Django is a 20-year-old project that carries a database layer, and I would not expect a routing library and an ORM to score the same.

DRF: what Django leaves to you

DRF sits on top of Django and fills in step 2. APIView.dispatch wraps the request, then initial runs content negotiation, versioning, authentication, permission checks and throttling, in that order, before your handler sees anything. Validation happens explicitly when your code calls serializer.is_valid().

It is a small repo: 250 files and 46,099 lines, with zero hotspots. Its longest function is get_field_kwargs in rest_framework/utils/field_mapping.py at 161 lines, which maps Django model fields to serializer fields. That one function is where "Django model" turns into "API field", so it makes sense that it carries a lot of cases.

One thing I liked: DRF has 59 recorded decisions, more than Django's 31 in our index, and most of them come from code comments explaining why something is done, like deferring rendering until the template response renders.

Flask: small on purpose

Flask's dispatch_request is about 25 lines. By the time it runs, Werkzeug has already matched the URL (Flask imports MapAdapter and Rule from werkzeug.routing). Flask looks up self.view_functions[rule.endpoint] and calls it with the URL arguments. There is no input validation layer. Flask leaves that to you on purpose: you bring marshmallow, Pydantic or your own checks.

The whole repo is 128 files and 14,389 lines, with zero hotspots. It still scores 7.0 because, in a repo with 83 Python files, a few large central files carry a lot of weight in the average. The Flask class lives in src/flask/app.py (1,628 lines) and src/flask/sansio/app.py (1,013 lines), and those two files are where the score loses most of its points.

That second file has a reason to exist. "Sans-IO" means code that knows nothing about how bytes arrive, sync or async. Flask keeps its core app logic there so that Quart, the async version of Flask from the same Pallets team, can reuse it: Quart's src/quart/app.py imports App from flask.sansio.app and adds the ASGI layer on top. Quart is 97 files and 11,287 lines. If you want an even smaller Flask-style framework, Microdot runs on both regular Python and MicroPython for microcontrollers, in 171 files and 10,651 lines, and scores 7.5.

FastAPI: the work happens before your function runs

FastAPI uses Starlette for the low-level routing and does most of its own work in step 2. get_request_handler builds the per-route function. On each request it calls solve_dependencies (in fastapi/dependencies/utils.py) to resolve your Depends(...) tree and validate query, path, header and body values with Pydantic, raises RequestValidationError if anything fails, then calls run_endpoint_function and serialize_response. Your type hints are the input spec.

The repo surprised me in two ways. Most of it is something other than the framework: of 2,927 files, 615 are tests, 465 are docs_src (the runnable examples behind the documentation), and the fastapi/ package is 52 files. And fastapi/routing.py is 6,527 lines at the indexed commit, and 2,372 of those lines are documentation strings inside Annotated[..., Doc("...")] parameters. The docs for every parameter live in the type annotations, so editors can show them. That makes files very long without making them very complex. The two top hotspots are the files you would guess, fastapi/routing.py and fastapi/dependencies/utils.py.

Litestar: a trie and msgspec

Litestar's ASGIRouter builds a routing trie at startup in construct_routing_trie. A trie (prefix tree) stores paths segment by segment, so lookup cost grows with the length of the path and stays flat as you add routes. _handle_routing is written to be cached. Once a route is found, HTTPRoute.handle picks the handler for the HTTP method and calls it.

Validation runs through litestar/_signature/model.py, where SignatureModel is a msgspec Struct. msgspec is a fast serialization and validation library, and Litestar builds a model from each handler's signature at startup instead of at request time.

It has the highest score of the seven (8.5) and only 2 hotspots. I think part of that is architecture: work moves to startup, and there are many small files. Part of it is timing: Litestar is preparing 3.0, and the recent decisions show a cleanup in progress, moving template integrations out of litestar.contrib into litestar.plugins and removing deprecated namespaces. I think a repo in the middle of a deliberate cleanup tends to score well, though I have no data that tests that.

Sanic: routing as generated code

Sanic's handle_request is one function of about 160 lines that runs request middleware, finds the route, calls your handler and runs response middleware. The routing itself lives in a separate package, sanic-routing, and the lookup in sanic/router.py is wrapped in an lru_cache of 1,024 entries.

The neat part is in sanic-routing: when the router is finalized, it renders the route tree into Python source code for a find_route function and compiles it, so your routes become a generated function. It is a clever way to make matching fast without writing C.

Sanic scores 7.2 with 10 hotspots. Our snapshot is from 31 May 2026, older than the others, and it does not have a maintainability score yet. I would not read much into the gap with FastAPI until it is reindexed.

Robyn: Python on top, Rust underneath

Robyn differs most from the rest: its HTTP server is actix-web, a Rust web server, and routing uses the Rust matchit crate inside src/routers/http_router.rs. The index function in src/server.rs runs before-request middleware, finds the route with router.get_route(...), calls your Python function through PyO3 (the Rust to Python bridge), then runs after-request middleware.

The Python side, robyn/router.py, mostly registers routes and formats responses. So the Python surface is small: 102 Python files, 27 Rust files, and a lot of JavaScript in the docs site. Robyn scores 7.4 on 36,839 lines.

How to read these scores

  • Within this group, size does not explain the order. The two smallest repos, Flask and Sanic, score 7.0 and 7.2, and FastAPI scores 8.4 at 101k lines. Only Django, four times bigger than the next one, fits the pattern across the whole index, where health drops as repos grow.
  • The score covers the whole repo, so tests, docs examples and build scripts count. FastAPI's 465 docs_src files are part of its number. That is fair for "how risky is it to change this repo", and unfair if you read it as "how good is the framework I import".
  • Snapshots are from different dates. Sanic's is from May, Litestar's and FastAPI's are from today. A month of active work can move a score.
  • Match the framework to your requirements. If you need an ORM, migrations and an admin, Django's 6.4 reflects a lot of features you would otherwise build. If you need a tiny WSGI app, Flask's size is the point.
  • The health score measures change risk, not speed or API design. Robyn's Rust core and Litestar's trie are speed choices that sit outside it.

Method and how to reproduce it

For each repo we used the latest ready repowise snapshot on 6 October 2026 (dates in the table). Health: repowise's health score (1 to 10) rates how likely a file is to cause bugs and how hard it is to change, using static checks plus git history; the repo score is the average across files, weighted by lines of code. Maintainability and static performance are scored separately and are not blended in. Scores are shown with one decimal, cut (not rounded) the same way the repo pages show them. Lines of code are non-blank, non-comment lines across every indexed file. Function lengths and import counts come from the parsed code. Code links point to the exact commit we indexed.

To reproduce a row, paste the repo's GitHub URL into repowise, or run repowise init in a local checkout and open the health and architecture views. The architecture map below is repowise's own repository, the same view each framework's /architecture page gives you.

repowise architecture graph of the repowise-dev/repowise repository, files grouped into module communities with import edges between them
repowise architecture graph of the repowise-dev/repowise repository, files grouped into module communities with import edges between them

FAQ

Which Python web framework has the cleanest codebase?
Litestar scores highest on repowise's health score at 8.5 with two hotspots, followed by FastAPI at 8.4. The score covers the whole repository, so tests and docs examples count.
Why does Django score lowest of the seven?
Django ships an ORM, migrations, an admin, auth, sessions, caching and templates in one 435,086-line repository, and 2,018 of its 3,010 indexed files are tests. It is also the only one of the seven where health falls with repo size.
How does FastAPI validate requests?
get_request_handler builds a per-route function that calls solve_dependencies, resolves the Depends(...) tree and validates query, path, header and body values with Pydantic. Failures raise RequestValidationError before your function runs.
Why is Robyn different from the other frameworks?
Its HTTP server is actix-web in Rust, routing uses the matchit crate, and Python handlers are called through PyO3. The Python side mostly registers routes and formats responses.
Is the health score a measure of framework quality?
No. It rates how likely a file is to cause bugs and how hard it is to change, from static checks and git history. It does not measure speed or API design.