The repository or configuration that created this release differ from a previous release. View details.
wheelgap
Find the Python dependencies that have no prebuilt wheel for your target platform, before your Docker build does.
Point it at your lockfile and the platforms you ship to (ARM servers, Alpine images, Windows on ARM, free-threaded Python 3.14t). It tells you which packages pip or uv will have to compile from source, and which can't be installed at all.
📊 New: wheel coverage for the top 5,000 PyPI packages, by platform and Python version, updated nightly.
Install and run (under 30 seconds)
With uv:
uvx wheelgap
Or with pipx:
pipx run wheelgap
Or install it: pip install wheelgap.
Run either one in a project directory. It finds uv.lock, pylock.toml, poetry.lock, pdm.lock, Pipfile.lock or requirements.txt on its own.
Usage
# Will my Alpine ARM image compile anything?
wheelgap -p 3.13 -t alpine-arm64
# Is my stack ready for free-threaded Python 3.14?
wheelgap -p 3.14t -t all
# Several Pythons at once, Linux on both architectures
wheelgap uv.lock -p 3.12,3.13,3.14 -t linux-x86_64,linux-aarch64
# No -p? A .python-version file next to uv.lock (written by uv or pyenv) sets the Python
wheelgap uv.lock -t linux-x86_64
# Check a Pipenv project's default and development dependencies
wheelgap Pipfile.lock -p 3.13 -t linux-x86_64,linux-aarch64
# Check a PDM project's locked distribution files
wheelgap pdm.lock -p 3.13 -t linux-x86_64,linux-aarch64
# No lockfile? Check packages directly (latest version, or pin one)
wheelgap psycopg2 uvloop numpy==2.3.4 -t windows,win-arm64
# Take the target from the last FROM line (e.g. python:3.13-alpine
# or ghcr.io/astral-sh/uv:0.13.7-python3.13-trixie-slim)
wheelgap --dockerfile Dockerfile
# Machine-readable, for scripts and CI
wheelgap --json > wheelgap.json
# Why is it a gap? List each rejected wheel and the reason
wheelgap -t alpine-arm64 --explain
Projects usually ship to the same platforms, so you can write the defaults down once in pyproject.toml (next to the lockfile, or in the current directory). Command-line flags still win.
[tool.wheelgap]
python = ["3.13", "3.14t"]
targets = ["linux-x86_64", "linux-aarch64", "musl-aarch64"]
glibc = "2.31"
Each cell in the table is one of:
| cell | meaning |
|---|---|
✓ |
a compatible wheel exists, so the install is fast |
● source |
no wheel, only an sdist, so it compiles at install time (needs a compiler, headers, time) |
✗ missing |
no wheel and no sdist, so the install fails |
– |
not installed on this target (environment markers exclude it) |
✓ bundled |
no wheel on PyPI, but Pyodide ships its own build (with --pyodide-lock) |
Packages that are fine everywhere are hidden; --all shows them.
--explain lists, for each gap, the wheels that were rejected and why, closest misses first:
armlib 1.4.0 on musl-aarch64: ● source
armlib-1.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl manylinux_2_17_aarch64 is a glibc wheel; musl (Alpine) needs musllinux
For uv.lock inputs, --explain also shows why the package is installed at all: the shortest dependency path from your project (or from a dependency group, shown as (dev group)) to the gap package. Paths are worked out per target, since markers can pull a package in on one platform only. With --json --explain, each package gets a matching via object keyed by target, like rejected: "via": {"win-arm64": ["myapp", "uvicorn", "uvloop"]}. Other inputs have no dependency graph, so they show no path (and "via": {}).
uvloop 0.21.0 on win-arm64: ● source
via: myapp → uvicorn → uvloop
If you think wheelgap got a package wrong, that output (or --json --explain) is all a bug report needs, with no need to share your lockfile.
Exit codes: 0 no gaps, 1 gaps found, 2 bad input. --exit-zero always exits 0. --fail-on missing (or fail-on = "missing" in [tool.wheelgap]) accepts source builds and only exits 1 when a package can't be installed at all.
Use -q / --quiet (or quiet = true in [tool.wheelgap]) to show only the gap summary and package names. Clean runs are silent, JSON output is unchanged, and quiet text output cannot be combined with --explain (--json --explain works).
Targets
Run wheelgap --list-targets to see target aliases and compatibility limits, or add --json for a machine-readable list.
| name | aliases | wheel tags it accepts |
|---|---|---|
linux-x86_64 |
linux, linux-amd64, linux/amd64 |
manylinux_2_*_x86_64 up to --glibc (default 2.36), legacy manylinux2014/2010/1 |
linux-aarch64 |
linux-arm64, linux/arm64, linux/arm64/v8 |
manylinux_2_*_aarch64 |
musl-x86_64 |
alpine, musl |
musllinux_1_*_x86_64 up to --musl (default 1.2) |
musl-aarch64 |
alpine-arm64 |
musllinux_1_*_aarch64 |
macos-arm64 |
macos, mac |
macosx_*_arm64 and universal2 up to --macos (default 14.0) |
macos-x86_64 |
macos-intel |
macosx_*_x86_64 and universal2 |
win-amd64 |
windows, win |
win_amd64 |
win-arm64 |
windows-arm64 |
win_arm64 |
pyodide |
wasm, wasm32, emscripten |
Pyodide's ABI for that Python: pyodide_2024_0 (3.12), pyodide_2025_0 / pyemscripten_2025_0 (3.13), pyemscripten_2026_0 (3.14) |
Pyodide (Python in the browser) only installs wheels: there's no compiler, so a package with only an sdist shows as ✗ missing, not ● source. Pyodide wheels are tied to one ABI per Python version and don't work across them. Pyodide also ships its own builds of many popular packages (numpy, pydantic-core and others) that aren't on PyPI. Point --pyodide-lock at the release's pyodide-lock.json (a path or URL, also pyodide-lock = "..." in [tool.wheelgap]) and those count as ✓ bundled, with a note when Pyodide's version differs from the one you pinned:
wheelgap -p 3.12 -t pyodide --pyodide-lock https://cdn.jsdelivr.net/pyodide/v0.27.7/full/pyodide-lock.json
Docker platform names also work with -t, for example -t linux/amd64,linux/arm64 (also linux/arm64/v8).
-t all checks the eight native platforms; Pyodide is only checked when you name it. Python versions take a trailing t for free-threaded builds (3.14t), which correctly rejects abi3 wheels. Without -p, wheelgap reads a .python-version file next to the lockfile (or pyproject.toml) if there is one, and otherwise uses the Python it is running on.
In CI (GitHub Action)
- uses: actions/checkout@v7
- uses: wheelgap/wheelgap@v0.5.0
with:
args: uv.lock -p 3.13 -t linux-x86_64,linux-aarch64
The job fails when a dependency would build from source on a target you ship, for example when a dependency bump drops ARM wheels.
With pre-commit
repos:
- repo: https://github.com/wheelgap/wheelgap
rev: v0.5.0
hooks:
- id: wheelgap
The hook runs when uv.lock, poetry.lock, pdm.lock, pylock.toml, Pipfile.lock or a requirements*.txt file changes. Put your targets in [tool.wheelgap] in pyproject.toml so the hook checks the right platforms. Pass extra options with args:, for example args: [--fail-on, missing].
From Python
import wheelgap
targets = wheelgap.make_targets(python="3.13", platforms=["linux-aarch64", "musl-aarch64"])
result = wheelgap.check_package("numpy", "2.3.4", targets) # looks the files up on PyPI
result.ok # True if every target has a wheel
result.gaps # the targets that don't
result.to_dict() # {"name": ..., "results": {"3.13 musl-aarch64": "wheel", ...}}
for r in wheelgap.check_lockfile("uv.lock", targets): # markers and the uv graph respected
print(r.name, r.version, [t.platform for t in r.gaps])
check_packages([...]) checks many packages in parallel, and check_package(..., files=[...]) works offline from filenames you already have.
Scanning the top PyPI packages
python -m wheelgap.scan --top 5000 --out coverage.json
This checks the latest release of the most-downloaded packages (from top-pypi-packages) against every platform, for Python 3.13, 3.14 and 3.14t by default, and writes one JSON file with per-platform totals. It's the data behind the coverage tracker on the roadmap.
How it works
- Lockfiles are read offline.
uv.lock,pylock.toml,poetry.lockandpdm.lockalready record every file published for each pinned version. wheelgap parses the wheel filenames' tags withpackagingand checks them against the exact tag set each target accepts. That is the same logic pip and uv use to pick a wheel. requirements.txtand bare package names are looked up on the PyPI JSON API. Feed it a fully pinned file (fromuv pip compile,pip-compileoruv export): wheelgap checks the lines you give it and doesn't resolve dependencies.Pipfile.lockreads bothdefaultanddevelopdependencies and looks up distribution filenames on PyPI for their pinned versions. Environment markers are preserved; Git, path, file and editable dependencies are skipped as non-index packages.- Environment markers are respected. A
pywin32 ; sys_platform == 'win32'dependency won't show up as a Linux gap. Foruv.lockthe dependency graph is walked per target, so transitive platform-specific packages are handled too. --onlinere-checks lockfile packages against PyPI. Use it when a project has uploaded new wheels for an existing version since you locked, or if youruv.lockwas generated withenvironmentsrestrictions.
Why this exists
Missing wheels show up late, in the most expensive place:
- Your
python:3.13-alpineimage on an ARM runner takes 25 minutes because two transitive dependencies don't publishmusllinux_aarch64wheels, and QEMU is compiling them. - A build fails with
error: Microsoft Visual C++ 14.0 or greater is requiredon a teammate's Snapdragon laptop. - You try free-threaded Python 3.14t and half your stack quietly falls back to source builds, because
abi3wheels don't apply there.
Sites like pyreadiness.org and free-threaded wheels track the top few hundred packages. wheelgap checks your packages, at your pinned versions, for your targets, in about a second, and it can run in CI.
Roadmap
wheelgap is growing into caniuse for Python wheels: an open map of which packages have wheels for which platforms. The CLI stays focused, and around it come a nightly tracker of the most-used PyPI packages, a community knowledge base of workarounds, and campaigns to help upstream projects ship missing wheels. Read the roadmap and join the discussion →
Next up for the CLI: iOS and Android targets (#11), private indexes (#19), and "nearest version with a wheel" hints (#20).
Contributions welcome. Issues labelled good first issue are a good start, and CONTRIBUTING.md has the 3-command dev setup. Questions are welcome in Discussions.
Maintainer
wheelgap is created and maintained by @patrickballard, with help from its contributors.
License
Metadata
Release files for wheelgap 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Change of provenance
The repository or configuration that created these release files differ from that of a previous release.
We recommend inspecting the repository and configuration before proceeding. View details.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| wheelgap-0.5.0.tar.gz | 330.5 kB | With provenance | Details for wheelgap-0.5.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| wheelgap-0.5.0-py3-none-any.whl | Python 3 | none | any | Details for wheelgap-0.5.0-py3-none-any.whl |
Total release size: 371.7 kB
Release files / wheelgap-0.5.0.tar.gz
Change of provenance
This file belongs to a release with changed provenance: the repository or configuration that created the release files differ from that of a previous release.
We recommend inspecting the repository and configuration before proceeding. View details.
| Download URL | wheelgap-0.5.0.tar.gz |
|---|---|
| Size | 330.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b40983357951149349a457599958ad57eef932424db6005890bff89365537aec
|
|
BLAKE2b-256 checksum How to use checksums |
8240c4ba5a1190f6a982e1a3596a97db8c50e36f7984e5d832c9d7793fecfd95
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logRelease files / wheelgap-0.5.0-py3-none-any.whl
Change of provenance
This file belongs to a release with changed provenance: the repository or configuration that created the release files differ from that of a previous release.
We recommend inspecting the repository and configuration before proceeding. View details.
| Download URL | wheelgap-0.5.0-py3-none-any.whl |
|---|---|
| Size | 41.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3219b12e77275d8b5c75f4278d07de09881142713c7280e8ba70e8d6f5e7df2d
|
|
BLAKE2b-256 checksum How to use checksums |
b5b3a45fb5a74b0b1c2f18813c3b19022e7dc7db0a684476ee912235e8bc00f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logSecurity for wheelgap 0.5.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Change of provenance
The repository and configuration that created this release differ from a previous release.
While this could reflect a routine change by the project maintainers, it may also indicate that the release files have been compromised.
Repository
- Removed 0.4.0 github.com/patrickballard/wheelgap
- Added 0.5.0 github.com/wheelgap/wheelgap
Attestation for wheelgap-0.5.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 9, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.