Software Atlas · Sheet No. 2 · Notes on an unmappable coast

The PyPI Problem

Ask the atlas to survey npm/express and it charts 68 repositories without complaint. Ask it to survey pypi/prefect or pypi/dagster and the expedition returns empty-handed. This sheet explains why — and why the failure is a fact about Python packaging, not a bug in the instrument.

§ 0 The observation

The atlas resolves dependency closures with Google's deps.dev. For every package version, deps.dev exposes two different answers:

Here is what deps.dev said when we asked (checked 2026‑07‑03; re-check live below):

Package:requirements (the recipe):dependencies (the meal)
pypi/flask@3.1.3200 — 9 constraints200 — resolved graph
pypi/prefect@3.7.7200 — 83 constraints404 — declines to answer
pypi/dagster@1.13.12200 — 100 constraints404 — declines to answer

deps.dev knows what prefect asks for. It refuses to state what prefect gets. That distinction — recipe versus meal — is the entire subject of this sheet.

Instrument No. 1 · Interrogate deps.dev yourselflive

Any package, any of the atlas's ecosystems. We ask deps.dev both questions and report the raw status codes.

§ 1 Reason one — the manifest is a program

In npm, a package's dependencies are data. package.json is a static file; the registry reads it once at publish time and knows, forever and for everyone, what the package depends on. The map is printed on the territory.

Python grew up differently. For most of its history a package declared dependencies in setup.py — an arbitrary program that computes the dependency list at install time, on the installing machine. You cannot know what it will say without running it, and a registry cannot safely run millions of strangers' programs.

Model No. 1 · The manifest is a programtoy example
# setup.py — executed at install time, not read
import os, sys
from setuptools import setup

deps = ["requests>=2.28"]

if sys.platform == "win32":              # which OS is running me?
    deps.append("pywin32>=306")
if os.environ.get("MYPKG_GPU") == "1":   # what's in the shell environment?
    deps.append("cupy-cuda12x")

setup(name="mypkg", install_requires=deps)

Run the same file in four places:

The modern ecosystem has a fix — static metadata in pyproject.toml (PEP 621) and reliably-static sdist metadata (PEP 643) — and well-run projects use it. Prefect and Dagster both publish wheels with fully static dependency lists. So this reason alone explains the long tail of PyPI, but not our two packages. For them, the problem is deeper.

Sdist? Wheel? Sheet No. 3 is the field guide to Python packaging — the two boxes opened, filenames decoded.

§ 2 Reason two — one manifest, many worlds

Even a perfectly static Python dependency list does not name a dependency set. It names a family of dependency sets, indexed by the installing environment. Every line may carry an environment marker — a predicate over the Python version, operating system, interpreter implementation — and an extra gate. The npm question “what does express depend on?” has one answer. The Python question is unanswerable until you finish the sentence: “what does prefect depend on when installed where, on which Python, with which extras?

This is not hypothetical. The model below evaluates the actual published metadata of prefect 3.7.7, dagster 1.13.12, and flask 3.1.3 (snapshot 2026‑07‑03). Flip the world and watch the dependency set change:

Model No. 2 · One manifest, many worldsreal metadata, snapshot 2026‑07‑03

Two details worth savoring in prefect's real metadata: below Python 3.13 it depends on pendulum, but at 3.13+ it swaps that entire subtree for whenever — two different date-time stacks depending on the interpreter you happen to run. And dagster ships a dependency that exists only on Windows. A database that stores “the” dependency graph of these packages must either pick one world, store them all, or stay silent.

§ 3 Reason three — resolution is a search, not a lookup

Suppose you fix a world: CPython 3.12, Linux, no extras. You still don't have a graph — you have constraints, and someone must now choose concrete versions that satisfy all of them simultaneously. Here the two ecosystems diverge structurally:

Model No. 3 · One flat environment vs. a nested treetoy example
Python — one slot per package
npm — every package gets its own copy
app/
└─ node_modules/
   ├─ alpha 2.0
   │  └─ node_modules/
   │     └─ pydantic-js 1.10   ← alpha's private copy
   └─ beta 3.1
      └─ node_modules/
         └─ pydantic-js 2.7    ← beta's private copy

no shared slot → no conflict → no search.
the graph is knowable in advance.

The same scenario never conflicts: each library keeps its own copy of the disputed package. This is why a registry can precompute npm graphs wholesale.

And note the epilogue on the left when the search completes: the answer depends on which versions existed on the day you asked. A resolved Python graph is stamped with a date; tomorrow's release can change it.

§ 4 What deps.dev does about it

“Dependencies are the resolution of the requirements (dependency constraints) specified by a version. The dependency graph should be similar to one produced by installing the package version on a generic 64-bit Linux system, with no other dependencies present.” — deps.dev API documentation, GetDependencies

deps.dev picks one synthetic world and resolves inside it, where it can. Flask — nine constraints, one trivial marker — resolves fine. Prefect and Dagster — 83 and 100 constraints, dozens of markers, sprawling extras matrices, heavyweight transitive closures — do not get a published resolution, and deps.dev returns 404 rather than guess.

Read charitably, the 404 is the honest move. It is the same policy this atlas uses for its own gaps: say plainly that you don't know rather than print a confident wrong number. The recipe is public; the meal is declined.

§ 5 What this means for the atlas

The atlas's answer combines two moves. First, the one it already made for counting lines: push the ambiguity into the stated boundary. Second, a realization about fidelity: the atlas counts every repository at HEAD, not at a resolved version — so it never needed the version assignment deps.dev can't provide. Package names are the honest resolution for a LOC survey.

So when deps.dev declines a PyPI closure, the atlas now walks the declared requirements by name — recursively, markers evaluated against one declared world (CPython 3.12 · Linux x86‑64 · no root extras, each package at its PyPI-default version), each unique source repo still counted exactly once. The survey result carries that boundary as a visible flag. The number stops pretending to be universal and becomes true relative to its frame — which is the only kind of true this territory offers. Prefect and Dagster, unmappable at the top of this sheet, are surveyable under it.

What's still out of reach: sdist-only packages whose setup.py computes its dependencies (§ 1) — those stay uncharted, and the survey says so: here be dragons.