Stop guessing why a wheel is absent, whether it matches, or whether its archive is consistent.
WheelWhy explains index candidates and local wheel Python, ABI, and platform components as
correlated evidence around the authoritative complete tag. It also validates internal metadata and
can stream-verify every RECORD hash and size without extracting, importing, or installing package
code. Local-artifact commands make no network requests; the explicit availability command makes
one bounded metadata request.
The screenshot uses synthetic demo wheels created by scripts/make_demo_wheels.py.
pip can reject a local artifact with one sentence:
ERROR: ... is not a supported wheel on this platform.
That sentence hides distinct fixes: use the right Python, download the right CPU build, choose a compatible operating-system wheel, avoid a debug ABI, or repair contradictory wheel metadata. pip issue #10793 has requested cause-specific errors since January 2022 and remains open. Its examples distinguish several causes, while a pip maintainer clarification correctly notes that the three tag components interact and cannot be treated as independent compatibility verdicts. The same generic failure continues to recur in recent user reports.
WheelWhy turns the declared tag/metadata decision into inspectable evidence and adds a separate, installer-independent archive integrity result:
$ wheelwhy explain tensor_demo-1.0-cp313-cp313-win_amd64.whl \
--python 3.12 --platform win_amd64
WheelWhy BLOCKED
tensor_demo-1.0-cp313-cp313-win_amd64.whl
Declared tags/metadata: NO MATCH for CPython 3.12 on win_amd64
x Python cp313
no supported complete tag for CPython 3.12 on win_amd64 uses wheel interpreter value cp313
x ABI cp313
no supported complete tag for CPython 3.12 on win_amd64 uses wheel abi value cp313
~ Platform win_amd64
wheel platform value win_amd64 appears only in other supported complete tags
Next
1. No supported complete target tag uses this Python component; obtain a wheel for cp312.
2. No supported complete target tag uses this ABI component; rebuild for a valid target ABI.
3. Obtain or build a wheel with a supported complete tag such as
cp312-cp312-win_amd64, py3-none-any, or cp312-abi3-win_amd64.WheelWhy has one runtime dependency: PyPA's packaging, the
same maintained tag implementation used throughout Python packaging.
Install the verified wheel from the latest GitHub release:
python -m pip install https://github.com/CAOShurong/wheelwhy/releases/download/v0.3.1/wheelwhy-0.3.1-py3-none-any.whlOr download the self-contained zipapp and run it without installing:
python wheelwhy.pyz doctor
python wheelwhy.pyz explain path\to\package.whlRelease assets include SHA256SUMS and GitHub build-provenance attestations. WheelWhy is not yet
published on PyPI; pip install wheelwhy is therefore not a documented install path.
wheelwhy availability "numpy==2.3.0" --python 3.9 --platform win_amd64availability makes exactly one anonymous HTTPS request to the selected project's
PEP 691 Simple API
endpoint. It does not download a wheel, source archive, metadata sidecar, interpreter, or
dependency. For the selected release it reports stable reasons such as file.requires_python,
file.yanked, file.sdist, wheel.python_mismatch, wheel.abi_mismatch, and
wheel.platform_mismatch, plus the newest compatible wheel release inside the same specifier when
one exists.
The screenshot is an observed PyPI response captured on 2026-08-11. Index contents can change; JSON fixtures in the test suite make the reason and ordering contract reproducible.
Only one project requirement is accepted. Extras, markers, direct URLs, credentials, query strings,
insecure HTTP, authenticated indexes, multiple indexes, and dependency resolution are deliberately
out of scope. --index-url can select one anonymous PEP 691-compatible HTTPS index, and --timeout
is bounded to 60 seconds.
wheelwhy package-1.0-cp312-cp312-win_amd64.whlThe explain subcommand is optional when the first argument is a wheel path.
wheelwhy verify package-1.0-py3-none-any.whl
wheelwhy verify package-1.0-py3-none-any.whl --jsonverify first performs the same declared target check, then streams the archive and validates its
required WHEEL, METADATA, and RECORD contract. It reports all deterministic findings with
stable codes such as record.hash_mismatch, record.phantom_path, archive.unsafe_path, and
wheel.tag_mismatch. It does not extract files.
wheelwhy explain package-1.0-cp312-cp312-win_amd64.whl --filename--filename is explicit because a misspelled path should not silently become a successful
filename-only check. Archive metadata is marked uninspected in this mode.
wheelwhy explain package.whl --python 3.12 --platform manylinux_2_17_x86_64
wheelwhy explain package.whl --python 3.10 --implementation pp \
--abi pypy310_pp73 --platform manylinux_2_17_x86_64Synthetic targets are driven by explicit wheel platform tags. WheelWhy does not pretend to detect a remote machine's libc, macOS deployment target, or CPU.
wheelwhy choose dist/
wheelwhy choose dist/ --python 3.12 --platform win_amd64 --jsonThe selected wheel is the compatible artifact with the highest preference in packaging.tags.
Rejected candidates retain per-axis reasons.
wheelwhy tags --limit 30
wheelwhy doctor --json| Code | Meaning |
|---|---|
0 |
A compatible wheel exists; for verify, structure and integrity also pass. |
1 |
Deterministic unavailability/404, declared incompatibility, metadata/format defect, integrity failure, or no matching candidate. |
2 |
Invalid arguments/path, network/auth/protocol failure, unreadable archive, or a resource-limit refusal. |
--json emits schema_version: 1 and sends expected incompatibility results to stdout. Human input
errors go to stderr; JSON input errors remain JSON on stdout for reliable automation.
Each JSON axis has a status: match means the component participates in a complete matching
wheel tag, contextual means it occurs only in other supported complete tags named in
related_target_tags, and mismatch means no supported complete tag uses that component. The
existing axis compatible boolean is true only for match, so component membership is never
presented as unconditional compatibility.
Integrity findings contain stable code, kind, severity, member, and message fields.
Adding a new issue code is additive; scripts should use integrity.valid or the process exit before
depending on individual codes.
- The complete wheel tag triple, in installer preference order, as the authoritative verdict.
- Python/interpreter, ABI, and platform component evidence correlated back to complete target tags.
Requires-Pythonagainst the selected target.- Filename project/version against
.dist-info/METADATA. - Filename tags against
.dist-info/WHEELTagfields. - Required singleton
Wheel-VersionandRoot-Is-Purelibdeclarations plus at least oneTag. - Core Metadata fields through PyPA
packaging.metadata, including malformed or repeated fields. - With
verify: strict three-columnRECORD, safe relative paths, archive/manifest coverage, duplicate members and rows, fixed-output SHA-256-or-stronger hashes, and recorded sizes. - Resource limits: 1 MiB per metadata member, 16 MiB for
RECORD, 100,000 archive members, and 8 GiB declared uncompressed content. Wheel contents are streamed, never extracted or run. - With
availability: one PEP 691 JSON response capped at 8 MiB, project/release selection,Requires-Python, yanked state, source-versus-wheel type, and complete target tag triples.
WheelWhy explains declared tag and inspected-metadata compatibility using PyPA packaging. It
does not call pip or uv's complete resolver and does not claim exact behavioral equivalence with
either installer. availability uses its own documented deterministic release policy: prefer the
newest non-yanked stable release satisfying the specifier, fall back to a matching pre-release when
no stable one exists, and show an exactly pinned all-yanked release as unavailable evidence. That is
an explanation policy, not pip/uv candidate selection. Dependency resolution, environment markers,
multiple-index precedence, build policy, wheel variants, installer version, and other installer
checks can still change the outcome. A listed source distribution is not proof that it builds.
WheelWhy also does not prove that native code is safe or functional, inspect shared-library
dependencies, repair/build/install wheels, or replace testing on real target systems. A malicious
or incorrectly compiled binary can carry plausible tags and valid hashes; RECORD proves
self-consistency, not publisher identity or provenance. Use
auditwheel,
delocate,
delvewheel,
abi3audit, or platform testing for those jobs.
The design decision and alternative comparison are documented in
docs/RESEARCH.md. Security reporting is in SECURITY.md.
python -m venv .venv
.\.venv\Scripts\python -m pip install -e ".[dev]"
.\.venv\Scripts\python -m pytest --cov=wheelwhy --cov-report=term-missing
.\.venv\Scripts\python -m ruff check .
.\.venv\Scripts\python -m mypy
.\.venv\Scripts\python -m buildSee CONTRIBUTING.md for the compatibility-fixture rules and contribution flow.
WheelWhy is available under the MIT License. Release wheels include WheelWhy's license
and the selected upstream BSD notice for PyPA packaging; the self-contained zipapp includes both
licenses alongside the bundled code.



