open atlas
↑ Back to track
Python for JS/TS developers PY · 10 · 03

Publishing and lockfiles: wheels vs sdists, pinning by consumer, and trusted publishing

Wheels unpack; sdists build — platform tags decide which you get. Libraries declare ranges, applications lock the resolved graph with hashes; --require-hashes survives a hijacked release. Publish via build then uv/twine; trusted publishing uses short-lived OIDC.

PY Senior ◷ 18 min
Level
FoundationsJuniorMiddleSenior

Friday, 18:40. Every fresh CI run goes red with AttributeError deep inside an HTTP stack nobody touched. The diff under suspicion is a one-line README change; the engineer who pushed it spends two hours bisecting commits that cannot possibly be guilty. Local machines stay green the whole time — which makes it worse, because now the failure looks environmental and nobody trusts the runner. The actual event happened an hour earlier and three dependency levels down: a transitive dependency of the team’s HTTP client released 2.0 with a renamed module. requirements.txt pinned the client, not its dependencies; every fresh resolve picked up the new major, while laptops kept their cached 1.x in existing venvs. Nothing in the repo changed, so no commit could explain it — the input that changed was the package index itself. The lockfile that landed next sprint — full resolved graph, exact versions, artifact hashes — did not make dependencies safer. It made installs deterministic: the index stopped being a runtime input, and that entire class of Friday incident ended.

The artifact ladder: sdist and wheel

When you next wonder why installing a package on Alpine took ten minutes when it works instantly on your laptop, or why a pip install in CI unexpectedly ran a compiler — the answer lives in which artifact type was available and what that tag encodes.

A release on PyPI is usually two artifacts. The sdist is source plus a build recipe: to install it, the installer must run the build backend on the user’s machine — which may mean compiling C, finding headers, and executing arbitrary build code at install time. The wheel is the finished product: a zip the installer just unpacks into site-packages — no execution, no compiler, install measured in milliseconds. Which wheel applies is encoded in platform tags: py3-none-any is pure Python, one wheel for everyone; cp312-cp312-manylinux_2_28_x86_64 is compiled for CPython 3.12 against a baseline glibc on x86-64. The installer picks the most specific compatible wheel and falls back to the sdist only when none fits — which is exactly when your Alpine container suddenly needs gcc and ten minutes, or fails on a missing header. Two operational consequences: publish wheels for every platform you claim to support (pure-Python projects get this free with one any wheel), and notice the security asymmetry — installing a wheel runs no project code, installing an sdist executes the build backend and whatever the recipe pulls in.

Pinning philosophy: what you are, decides how you pin

The question “should I pin?” has no answer until you say who is installing. A library is resolved together with strangers — your httpx constraint must intersect with every other package’s httpx constraint in some application you will never see. So libraries declare honest, wide ranges: a lower bound you actually test against, an exclusion for a known-bad release (!=2.31.1), and no upper cap without a known incompatibility. The dependency-hell mechanism of over-pinning is arithmetic, not ideology: a resolver must select ONE version per package satisfying all constraints; every frivolous < cap shrinks the feasible set, and two well-meaning caps from unrelated libraries can make it empty — resolution fails, or backtracks into archaeology, installing versions years old because those predate the caps. An application is the opposite case: nobody resolves against you, and your job is that Tuesday’s deploy equals Friday’s. Applications lock everything — the full resolved graph, every transitive dependency, exact versions with hashes (uv.lock, or requirements.txt generated with hashes). Ranges express intent in pyproject.toml; the lockfile records the decision.

Quiz

Your library declares httpx>=0.27,<0.28 'to be safe'. Months later, apps that use your library plus another one requiring httpx>=0.28 stop resolving entirely. What is the mechanism?

What a lockfile actually records

A lockfile is a resolved graph snapshot: every package including transitives, the exact version, and the sha256 of every artifact. The hashes are the part people underrate — pip install --require-hashes (and uv’s default behavior with uv.lock) verifies every downloaded file against the recorded digest and fails closed on mismatch. That converts the lockfile from a reproducibility tool into a supply-chain control: an attacker who hijacks a maintainer account and re-releases a package cannot reach your CI, because the malicious artifact’s hash matches nothing you recorded. Cross-platform honesty: pip freeze on a macOS laptop is not a Linux lock — markers and platform wheels differ; uv resolves a universal lock with per-platform markers in one file, so the same uv.lock installs correctly on the dev laptop and the Linux runner. Locks also change incident response: the Friday hook becomes a non-event because a fresh CI run installs byte-identical inputs — upgrades happen when you re-lock, as a reviewed diff, not when an upstream maintainer has a Friday.

Publishing: build, upload, and trusted publishing

The flow is two commands: uv build (or python -m build) produces dist/*.tar.gz and dist/*.whl; uv publish (or twine upload) pushes them to PyPI. Rehearse against TestPyPI first — a separate index where you can validate metadata, the README render, and a scratch install before the name is burned (PyPI versions are immutable: you cannot re-upload a fixed 1.4.0, only release 1.4.1). The modern auth story is trusted publishing: instead of a long-lived API token sitting in CI secrets — leakable, rotatable, copied to three places — PyPI is configured to trust the OIDC identity of your CI workflow (this repo, this workflow file, this environment). Each publish run, the CI provider mints a short-lived identity token, PyPI verifies it and issues a minutes-lived upload token. Nothing long-lived exists to steal; a leaked credential expires before the postmortem doc is created.

Quiz

A maintainer account of a transitive dependency is hijacked and a malicious 1.4.7 goes up. Which CI install survives untouched: (a) ranges in requirements.txt, (b) ==1.4.6 pinned on your top-level deps only, (c) a lockfile install with required hashes?

Supply-chain hygiene: yanks, typos, and the blast radius

Three habits close most of the remaining surface. Yanked releases: when a maintainer yanks a broken version, resolvers skip it for range requests, but an exact == pin (or a hash-locked install) still gets it, with a warning — yank is an undo button for future resolutions, not deletion; your locked CI keeps working, and you upgrade past the yank on your own schedule. Typosquatting: reqeusts and friends sit on the index waiting for a mistyped pip install; a lockfile shrinks the exposure window to the single moment a human adds the dependency — every subsequent install replays reviewed names from the lock instead of re-trusting fingers. And the general principle the whole lesson has been circling: hashes pin artifacts, not names or versions. Names can be squatted, versions can be re-released by hijacked accounts; a sha256 recorded at review time and verified at install time is the one identity an attacker upstream of you cannot quietly swap. Together these three habits — yank awareness, lockfile replay, and hash verification — close different parts of the surface: yank handles maintainer mistakes, the lock handles re-releases and fresh-resolve drift, and the hash closes the loop by pinning identity rather than just version numbers.

Recall before you leave
  1. 01
    Explain sdist vs wheel — what each contains, what installation does, what platform tags encode, and the security asymmetry.
  2. 02
    Give the pinning philosophy by consumer, the over-pinning failure mechanism, what a lockfile records, and how trusted publishing kills the long-lived token.
Recap

A PyPI release is an sdist — source plus recipe, built on the consumer’s machine by the backend, compiler and all — and wheels, finished artifacts the installer merely unpacks, selected by platform tag and falling back to source only when nothing fits; the asymmetry is operational (Alpine needing gcc) and security-relevant (wheel installs execute nothing). Pinning is decided by who installs: a library is resolved alongside strangers, so it declares a tested floor, excludes known-bad releases, and avoids frivolous caps — the resolver needs one version satisfying all constraints, and every needless cap shrinks that feasible set toward failure or archaeology; an application answers to no one’s resolver and locks the entire graph, exact versions and sha256 hashes, so the index stops being a runtime input. That determinism is what ended the Friday incident in the Hook — the breaking 2.0 arrived three levels down, no commit explained the red CI, and the lock made fresh installs byte-identical; the hashes additionally fail closed against hijacked re-releases, which no top-level == pin can do. Publishing is build, TestPyPI rehearsal, upload — with trusted publishing exchanging the leakable long-lived token for a per-run OIDC-minted one. Yanked versions vanish from future range resolves but still serve exact pins; typosquats wait for fingers, and a lock means fingers are only trusted once, at review time. Now when you see a fresh CI run go red with an error nobody’s code introduced, or an Alpine container demand a compiler, or a question about whether to add an upper cap — you know which artifact type, which lockfile property, and which pinning philosophy applies.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

Apply this

Put this lesson to work on a real build.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.