Binary Wheels & Extension Modules
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the Python Reference bibliography. |
A pure-Python wheel such as birdtally's (py3-none-any) installs everywhere. A library with compiled code (C, C++, Cython, Rust,
Fortran) needs one wheel per Python version, ABI and platform it supports, plus an sdist for the rest. This page explains the tags
that make this work and the tooling that builds the matrix. The examples ran on 2026-10-22 on Linux x86-64 (glibc 2.39) with
CPython 3.14.6, pip 26.2.2, setuptools 84.2.1 and gcc; cibuildwheel 4.3.4 is described, not run here.
Platform compatibility tags
A wheel name ends in {python tag}-{abi tag}-{platform tag} (see the figure on
Building sdists & Wheels):
| Tag | Values |
|---|---|
Python |
|
ABI |
|
Platform |
|
Several tags can be combined with dots (a compressed tag set): py2.py3-none-any, or
manylinux_2_17_x86_64.manylinux2014_x86_64 for one file that carries both names.
An installer accepts a wheel if any of its tags is in the interpreter’s supported list, and prefers the most specific one.
pip debug --verbose prints that list in priority order; on this machine it had 1226 entries:
python -m pip debug --verbose
Compatible tags: 1226
cp314-cp314-manylinux_2_39_x86_64
cp314-cp314-manylinux_2_38_x86_64
cp314-cp314-manylinux_2_37_x86_64
…
cp314-abi3-manylinux_2_39_x86_64 (entry 40)
…
cp313-abi3-manylinux_2_39_x86_64 (entry 118)
…
py3-none-manylinux_2_39_x86_64 (entry 625)
…
cp314-none-any (entry 1210)
…
py3-none-any (entry 1212)
(entry numbers added). Version-specific wheels win over abi3 wheels, which win over pure ones. poetry debug tags prints the same
list for the project’s environment.
Source: PUG: Python tag, ABI tag, Platform tag, manylinux, musllinux, macOS, Android, iOS, Compressed tag sets, pip debug.
A small extension with the stable ABI
birdhash is a one-function C extension built with setuptools. py_limited_api restricts it to the limited API (here of
Python 3.10), so one binary works on every later CPython: the wheel gets the abi3 tag.
[build-system]
requires = ["setuptools>=77.0.3"]
build-backend = "setuptools.build_meta"
[project]
name = "birdhash"
version = "0.1.0"
description = "A tiny C extension: a fast hash for species names."
requires-python = ">=3.10"
license = "MIT"
# setup.py: still the place for extension modules with setuptools
from setuptools import Extension, setup
setup(
ext_modules=[
Extension(
"birdhash._speedups",
["src/birdhash/_speedups.c"],
py_limited_api=True,
define_macros=[("Py_LIMITED_API", "0x030A0000")],
)
],
options={"bdist_wheel": {"py_limited_api": "cp310"}},
)
python -m build
unzip -p dist/birdhash-0.1.0-cp310-abi3-linux_x86_64.whl birdhash-0.1.0.dist-info/WHEEL
Successfully built birdhash-0.1.0.tar.gz and birdhash-0.1.0-cp310-abi3-linux_x86_64.whl
Wheel-Version: 1.0
Generator: setuptools (84.2.1)
Root-Is-Purelib: false
Tag: cp310-abi3-linux_x86_64
The same wheel installed and ran on CPython 3.14.6 and 3.12.3 (birdhash.fnv1a("lapwing") gave 10264165852705436863 on both).
abi3 trades some speed and API surface for far fewer wheels. It does not cover the free-threaded builds (cp314t); for those,
PEP 803 defines abi3t, which setuptools supports since 84.2.0 (2026-10-19) for Python 3.15 and newer.
That wheel is still not publishable: its platform tag is linux_x86_64, which only says "built on some Linux", and PyPI rejects
it. Linux wheels for PyPI are built inside the manylinux or musllinux containers (old glibc, standard toolchain) and repaired
with auditwheel, which copies external shared libraries into the wheel and sets the manylinux tag; delocate does the same on
macOS and delvewheel on Windows. cibuildwheel runs all of that.
Rules for publishing binary wheels
-
Always upload the sdist too. Users on platforms without a wheel build from it; a binary-only release leaves them nothing.
-
Build for every supported CPython, including a new minor release as soon as its ABI is frozen (release candidate 1), or users of that version build from the sdist and need a compiler.
-
Mind the platform specifics: on Windows, the MSVC runtime and DLL search paths; on Linux, the oldest glibc you target; on macOS, the deployment target,
universal2or separate arm64 and x86-64 wheels, and weak linking to newer system APIs. -
Test the built wheels, not the build tree, on each platform.
cibuildwheel
cibuildwheel 4.3.4 (2026-10-22) builds and tests wheels for CPython 3.9 to 3.15, PyPy and GraalPy, on Linux (manylinux and
musllinux containers), macOS, Windows, Pyodide, Android and iOS, from a CI job per platform. It repairs each wheel (auditwheel,
delocate, delvewheel) and runs your tests against the installed wheel. Configuration lives in pyproject.toml:
[tool.cibuildwheel]
build = "cp310-* cp311-* cp312-* cp313-* cp314-* cp315-*"
skip = "*-musllinux_i686"
test-requires = "pytest"
test-command = "pytest {project}/tests"
[tool.cibuildwheel.linux]
manylinux-x86_64-image = "manylinux_2_28"
[[tool.cibuildwheel.overrides]]
select = "cp315-*"
test-skip = "*"
Recent changes worth knowing:
-
4.0.0 (2026-06-07) dropped Python 3.8, runs
abi3auditafter the repair step by default, and makesdelvewheelthe default Windows repair command. -
4.2.0 (2026-08-04) builds CPython 3.15 wheels by default (3.15.0rc1 is ABI-stable).
-
4.3.2 to 4.3.4 (October 2026) are security releases around the compromised eMudhra CA roots.
The release workflow that runs it (an sdist job, one cibw-* artefact per platform, then a publish job) is on
GitHub Actions & GitLab CI Release Workflows.
Extension backends
-
scikit-build-core (1.1.1) drives CMake; configuration in
[tool.scikit-build], CMake options through-C cmake.define.X=…. -
meson-python (0.22.1) drives Meson (
build-backend = "mesonpy"); options through-C setup-args=…; used by NumPy and SciPy. -
maturin (1.15.0) builds Rust crates with PyO3, cffi or uniffi bindings and produces manylinux-compliant wheels itself; see Python Interop with PyO3.
Poetry build scripts
poetry-core can run a build script before building a wheel, declared in [tool.poetry.build]; the documentation still calls
this "still considered unstable":
[tool.poetry.build]
script = "build_extension.py"
[build-system]
requires = ["poetry-core", "setuptools", "cython"]
build-backend = "poetry.core.masonry.api"
The script runs before poetry install installs the project, before poetry build builds, and in any PEP 517 build from source
or sdist by another frontend; it runs from the project root in a temporary environment, so its own requirements must be listed
in [build-system] requires (with version constraints, the documentation advises). The documentation’s snippets cover Cython, Meson
and maturin. For anything substantial, a dedicated extension backend (above) is the more robust choice. Not run here.
Why birdtally stays pure Python
birdtally reads CSV files and counts rows: the standard library is fast enough, and a compiled speed-up would multiply the
release work (a wheel matrix, CI on three operating systems, a compiler for users without a wheel) for no measurable gain. One
py3-none-any wheel serves every platform and every future Python its requires-python allows.