In July 2026, MLX shipped v0.32.0 for macOS ARM64. The wheel contained py.typed. It did not contain a single .pyi stub — every stub the previous release had was silently gone. Downstream type checking broke for users. The project's own test suite never noticed, because tests run against the source tree, not the wheel.
A month earlier, OpenSpace's built distribution silently dropped a tracked host_skills/ directory. Everyone who pip installed it got broken integrations.
Both bugs have the same root cause: nobody checks the artifact. twine check verifies your README renders. Nothing in the standard toolchain verifies that the files actually made it into the wheel or sdist you uploaded.
Source, sdist, and wheel are three different things
This is the part we all pretend isn't true. Your repo has the files. Your sdist might. Your wheel might not. Build backends, MANIFEST.in glitches, and package-data misconfigurations drop files quietly, and the failure mode is always the same: everything looks fine until a user installs the thing and something is missing.
I got tired of finding out from users, so I built a small tool that checks the artifact itself — the thing PyPI actually serves — instead of the repo.
What wheeltruth checks
wheeltruth is a stdlib-only Python CLI. Point it at your built artifacts:
pip install wheeltruth
wheeltruth check dist/*
With just a wheel, it verifies:
- RECORD completeness — every file in the zip is listed, every listed file exists, and sha256 hashes/sizes match. Tampered or hand-edited wheels fail.
-
Entry points resolve — every
console_scripts/gui_scriptstarget points at a module actually inside the wheel. No moreModuleNotFoundErroron first run. -
Typing stub consistency — if a package ships some
.pyistubs, every module should have one. A half-dropped stub set (the MLX case) gets flagged. - METADATA consistency — name/version match the wheel filename.
Give it both the wheel and the sdist and it diffs them — files tracked in the sdist's packages that never made it into the wheel (the OpenSpace case), and vice versa. It handles src/ layouts and ignores tests/, docs/, and friends.
Two more modes for the paranoid:
# Verify packages declared in pyproject.toml/setup.cfg actually exist in the wheel
wheeltruth check dist/* --project .
# Install into a throwaway venv and import every top-level module
wheeltruth check dist/*.whl --smoke
Exit code is 0 when clean, 1 when issues are found, so it drops straight into CI:
- name: Build
run: python -m build
- name: Verify artifacts
run: |
pip install wheeltruth
wheeltruth check dist/*
Honest limitations
v0.1 is check-only — it reports problems, it doesn't fix your build config. "Expected files" are heuristics inferred from the sdist or your project config, so exotic layouts may produce false positives. --smoke needs a working venv and takes a few seconds per wheel. And the stub check is a consistency check — it can't know what your previous release shipped.
One thing I can say: before publishing v0.1.0, I ran it against its own dist/. It came back clean. That's the bar — the tool has to survive its own check. Then I pointed it at 300 real wheels, and it found bugs in itself instead. That's the more useful story.
I scanned the top 300 PyPI wheels
I pointed wheeltruth at the 300 most-downloaded PyPI wheels (latest versions). The honest result: zero hash mismatches, zero RECORD gaps, zero metadata mismatches. The top of PyPI is remarkably intact — there is no "1 in N wheels are broken" stat here, and I'm not going to invent one. What the scan did catch: 45 flags that, on hand-checking, turned out to be false positives exposing 3 bugs in wheeltruth itself (vendored RECORD merging, entry-point re-exports, over-aggressive stub checks). Fixed in v0.2.0 — pip install -U wheeltruth to get the corrected checks. The tool held up; so did the ecosystem.
Links
- GitHub: https://github.com/hahahahahahahahah6/wheeltruth
- PyPI: https://pypi.org/project/wheeltruth/0.1.0/
Found a false positive, or a layout it chokes on? Open an issue — that's the fastest way to make the next version smarter.
Top comments (0)