DEV Community

hao li
hao li

Posted on

Your Python Tests Passed. Your Published Wheel Is Missing Files.

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/*
Enter fullscreen mode Exit fullscreen mode

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_scripts target points at a module actually inside the wheel. No more ModuleNotFoundError on first run.
  • Typing stub consistency — if a package ships some .pyi stubs, 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
Enter fullscreen mode Exit fullscreen mode

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/*
Enter fullscreen mode Exit fullscreen mode

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

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)