Addendum · reference analyzer preview, not yet enabled in the hosted product
This page re-examines FastAPI Structural Evolution: v0.20 → v0.129. The original page and its evidence are unchanged.
The same FastAPI versions re-examined with the reference analyzer and checked against the source.
Primary artifacts for auditing the evaluation and its source records.
This page re-examines the February showcase with the PViz reference analyzer, which is not yet enabled in the hosted product. The original report, metrics and snapshots are unchanged and remain the historical record. Every number here comes from new analyzer output on the same 9 source versions, or from an independent parse of the source.
How did FastAPI's internal structure evolve from v0.20 to v0.129: growth, coupling, cycles, test investment, and the effect of the Pydantic v2 migration?
The February study used the analyzer of that time (bundle schema v1.1) on the 9 versions it describes as tagged releases, and published a report, a metrics file and a compressed snapshot of each. Some properties of that format matter here. Python import edges were stored from provider to consumer, the reverse of today's convention, so importer and dependency counts were swapped for Python files. And imports that re-export names from a package's `__init__.py`, or import the package itself (`import fastapi`), were not recorded.
Each snapshot was matched to upstream source by content: for every file the snapshot hashed, the file at the pinned revision has the same hash (7,378 files checked, 0 differ). 8 snapshots are the release tags named in the study. The snapshot labelled `v0.129` is not the `0.129.0` tag: it matches `master` on 2026-02-17 (version string `0.129.0`), and this addendum uses that commit.
| Label | Ref | Commit | Date | Content check |
|---|---|---|---|---|
| v0.20 | 0.20.0 | 7f673cf4e946 | 2019-04-27 | 299 files, 0 differ |
| v0.41 | 0.41.0 | 5592fa0f6f6b | 2019-10-07 | 403 files, 0 differ |
| v0.61 | 0.61.0 | a6897963d5ff | 2020-08-09 | 535 files, 0 differ |
| v0.69 | 0.69.0 | 921b6f742ad6 | 2021-10-07 | 585 files, 0 differ |
| v0.85.2 | 0.85.2 | d0917ce01566 | 2022-10-31 | 802 files, 0 differ |
| v0.104.1 | 0.104.1 | 7e5afe2cb9bf | 2023-10-30 | 1,198 files, 0 differ |
| v0.115 | 0.115.0 | 40e33e492dbf | 2024-09-17 | 1,306 files, 0 differ |
| v0.119 | 0.119.0 | 2e721e1b0218 | 2025-10-11 | 1,174 files, 0 differ |
| v0.129 | da4135ce1eb4 | da4135ce1eb4 | 2026-02-17 | 1,076 files, 0 differ |
Reference analyzer (preview: not enabled in the hosted product), release-candidate image revision `08823e7d7662`, run offline on the recovered source with the product's own analysis call. The current legacy analyzer was run on the same source as a control, and the `fastapi/` package's imports were parsed independently with Python's `ast` module.
Every row except the first was computed for this addendum. The AST rows are the reference point: they list what Python itself imports.
| Source of the figure | v0.20 | v0.41 | v0.61 | v0.69 | v0.85.2 | v0.104.1 | v0.115 | v0.119 | v0.129 |
|---|---|---|---|---|---|---|---|---|---|
| Published (Feb 2026, v1.1) | 3 | 3 | none | none | 3 | 3 | 3 | 3 | 3 |
| Source: every `fastapi/` import (AST) | 3 | 3 | 6 | 6 | 7 | 7 + 3 | 7 + 3 | 7 + 7 | 7 + 4 |
| Source: without TYPE_CHECKING imports | 3 | 3 | 6 | 6 | 7 | 7 + 3 | 7 + 3 | 7 + 7 | 7 + 4 |
| Source: module-level imports only | 3 | 3 | 6 | 6 | 7 | 7 | 7 | 7 + 2 | 7 + 2 |
| Current legacy analyzer | 3 | 3 | none | none | 3 | 3 | 3 | 3 | 3 |
| Reference analyzer | 3 | 3 | 6 | 6 | 7 | 7 + 3 | 7 + 3 | 22 | 18 |
| Measure | v0.20 | v0.41 | v0.61 | v0.69 | v0.85.2 | v0.104.1 | v0.115 | v0.119 | v0.129 |
|---|---|---|---|---|---|---|---|---|---|
| Internal import relations in `fastapi/` (AST) | 47 | 56 | 67 | 74 | 77 | 95 | 94 | 125 | 105 |
| Reference: missing / extra vs AST | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 0 / 0 | 6 / 1 | 2 / 1 |
| Legacy: missing / extra vs AST | 9 / 1 | 9 / 1 | 14 / 1 | 14 / 1 | 14 / 1 | 24 / 2 | 24 / 2 | 41 / 3 | 29 / 3 |
Importer and import counts are distinct modules in the reference analyzer's import graph. The published fan-out row is quoted from the original metrics file.
| Measure | v0.20 | v0.41 | v0.61 | v0.69 | v0.85.2 | v0.104.1 | v0.115 | v0.119 | v0.129 |
|---|---|---|---|---|---|---|---|---|---|
| routing.py: imported by (reference) | 4 | 8 | 8 | 8 | 13 | 13 | 13 | 13 | 14 |
| routing.py: imports (reference) | 4 | 6 | 7 | 9 | 8 | 9 | 9 | 10 | 9 |
| routing.py fan-out as published | 4 | 6 | 7 | 9 | 12 | 12 | 12 | 12 | 13 |
| applications.py: imports (reference) | 3 | 7 | 9 | 11 | 12 | 11 | 10 | 11 | 11 |
| applications.py: imported by (reference) | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
| FastAPI route handlers registered (mostly documentation examples) | 227 | 339 | 470 | 545 | 750 | 1075 | 1151 | 1243 | 1087 |
| Cross-module call pairs inside `fastapi/` | 21 | 26 | 28 | 30 | 31 | 33 | 34 | 52 | 45 |
| Call sites resolved in repo / all | 546 / 2,057 | 799 / 3,049 | 1,180 / 4,430 | 1,391 / 5,200 | 1,862 / 6,980 | 3,151 / 13,168 | 3,126 / 13,837 | 3,439 / 13,168 | 3,029 / 14,599 |
**Cycles were never absent.** The source contains a cycle in every version: 3 modules at v0.20, 6 at `v0.61` and `v0.69` (where the study reported zero), 7 from `v0.85.2`, and a second cycle around the Pydantic compatibility layer from `v0.104.1` (3 modules). The study's largest cycle never exceeded 3 because the edges that close the larger cycle (`import fastapi` in `utils.py`, and the package's re-exports) were not recorded.
**Fan-in and fan-out were swapped for Python files.** The study's routing.py fan-out (4 to 13) is its number of importers: routing.py is imported by 4 modules at v0.20 and 14 at v0.129, and itself imports 4 to 9. Likewise, applications.py did not "absorb" imports at `v0.85.2`: it imports 3 modules at v0.20 and 12 at `v0.85.2`, growing steadily, and is imported only through the package's `__init__.py`.
**The reference analyzer is exact on 7 of 9 versions.** At `v0.119` and `v0.129` it attributes the imports of `fastapi/_compat` to the package's top-level `__init__.py` (an underscore-module resolution defect, present in the legacy analyzer too) and misses 6 and 2 relations, which merges the real cycles (7 + 7 and 7 + 4 modules) into one group (22 and 18). The legacy analyzer misses between 9 and 41 relations per version.
| Original finding | Status | Addendum |
|---|---|---|
| Within-file growth during the Pydantic v2 migration rather than between-file coupling | Retained | Not contradicted: the large files grew far faster than the import graph; this addendum does not recompute the original's line counts. |
| The early cycle was resolved at v0.61; cycle nodes hit zero once | Revised | A 6-module cycle through `fastapi/__init__.py` exists at v0.61 and v0.69. |
| The core cycle routing → utils → dependencies/utils, bounded at 3 modules | Revised | That loop closes only through an import inside `if TYPE_CHECKING:` in `utils.py` (line 23 at v0.129), which Python never executes. The cycle that does execute has 7 modules and closes through `import fastapi` (line 9). |
| routing.py fan-out grew to its maximum; it is the most-used and most complex module | Revised | routing.py's importers grew to 14; its own imports grew to 9. It remains a large, central module. |
| applications.py broke a zero-dependency streak at v0.85.2 | Revised | It imported other modules in every version; the zero was an importer count. |
| Test/source ratio volatility is a documentation artifact | Retained | Consistent with the framework inventory: most route registrations come from documentation examples. |
| (new) Route registrations | New | The reference analyzer lists FastAPI route handlers: 227 at v0.20, peaking at 1,243, 1,087 at v0.129, nearly all in `docs_src` tutorials. |
| (new) Call graph inside the package | New | Cross-module call pairs inside `fastapi/` grow from 21 to 45; the busiest are routing and applications calling `datastructures`. |
At `v0.85.2`, `fastapi/utils.py` line 8 is `import fastapi`, and line 18 imports `routing` under `if TYPE_CHECKING:`. `fastapi/__init__.py` imports `applications` and `routing`, which import `utils`, which closes the cycle at module load. The per-version AST results, the reference and legacy bundles and the comparison tables are in the downloads below.
The comparison covers the `fastapi/` package. File counts, line counts and edge totals in the original also included documentation examples and tests and used the v1.1 line measure; they are not restated here. Neither current analyzer separates `TYPE_CHECKING` imports from runtime imports for FastAPI, so their runtime cycle figures equal their full cycle figures; the AST rows give the split.
FastAPI's package has had an import cycle throughout its history. It grew from 3 to 7 modules and acquired a second cycle around the Pydantic compatibility layer; it was never resolved. The study's account of a resolved, then permanent but bounded 3-module cycle reflects what the earlier analyzer could see, not the code. Its observations about within-file growth and documentation-driven test ratios stand. With the reference analyzer the package's import structure is reproduced exactly in most versions, and route registrations and calls become visible. Its one resolution defect here, with underscore-prefixed modules, is documented above.