cibuildwheel 3.0.0 is out, with some very big additions. We’ve added
GraalPy, Python 3.14 (and 3.14t) betas, and iOS support! We’ve got several new
options: test-sources
, test-environment
, and (experimental)
pyodide-version
. We now fully use enable
(and PyPy requires using it), and
we no longer inject setuptools and wheel in build environments. Defaults have
changed, too: build
is now the default frontend, manylinux_2_28
is the
default manylinux image, with 32-bit linux now being opt-in. We’ve removed
support for Python 3.6 and 3.7, we now require 3.11+ to run cibuildwheel itself,
and EoL manylinux/musllinux images now need to be fully specified.
We’ve had some fantastic releases of cibuildwheel since my last post over 2.19, so I’ll include a few of the new features from those releases, too. I’ll also note a few of the features being worked on for future releases.
iOS wheels (3.0)
We now support building iOS wheels! Building wheels for iOS is a bit more complex than standard wheels, so there have been a few new options added to support this.
test-sources
First, you can’t access files on the host system from iOS. So we’ve introduced a
new setting, test-sources
. If you specify test-sources
, then anything you
list gets copied to the testing directory. For example, say you had this:
[tool.cibuildwheel]
test-command = "pytest {project}/tests"
Now, you can replace it with:
[tool.cibuildwheel]
test-command = "python -m pytest tests"
test-sources = ["tests", "pyproject.toml"]
Notice you no longer need placeholders like {project}
! If you have test
configuration, remember to copy those files in too (like pyproject.toml
). If
you have no test files outside your package, you can set this to []
to silence
the iOS warning.
This is optional for other platforms, but it is required for iOS. Only items
inside your package or in test-sources
are visible to the tests.
xbuild-tools
The second issue is that this is a cross-compile; you need to tell cibuildwheel
what tools are safe to use from the host. You can do that with the new
xbuild-tools
setting:
[tool.cibuildwheel.ios]
xbuild-tools = ["cmake", "ninja"]
It is probably a good idea to scope this to ios
for now; we might eventually
find a use for it for other platforms. If you are using CMake, you need 4.0 or
newer; use brew upgrade cmake
before running cibuildwheel on GitHub Actions
runners until they update.
test-command
The test-command
setting is special in iOS, as you aren’t talking to a shell.
Every test-command
entry must start with python -m
(we will add this for you
if the command is pytest
, but print a warning).
Architectures
iOS has three architectures. They are:
arm64_iphoneos
: for devices (can’t be tested)arm_iphonesimulator
: for iOS simulators on Apple Silicon (can be tested on AS)x64_64_iphonesimulator
: for iOS simulators on Intel machines (can be tested on Intel)
You can build all architectures by setting the architectures to all
; only the
native simulator architecture can be tested. auto
(the default) builds the
native device and simulator wheels on ARM, and just the simulator on Intel. You
should always distribute at least the device and ARM simulator wheel, as app
developers need both to test and ship. For now, the Intel simulator wheel is
nice too.
Wheels for NumPy
If you need a binary such as NumPy, currently there aren’t wheels uploaded to
PyPI (this should change eventually). For now, the
"https://pypi.anaconda.org/beeware/simple"
index should be added to get
wheels; remember to set PIP_ONLY_BINARY
to numpy
or :all:
, and
PIP_PREFER_BINARY
can be set as well. By the way, you currently need to use
the build
backend (which uses pip
to install); uv
doesn’t support iOS yet.
If you need to limit dependencies for iOS, you can do it by checking
sys_platform
for ios
, such as:
[dependency-groups]
test = [
"pytest",
"pytest-xdist; sys_platform != 'ios'",
]
(iOS doesn’t have processes, so no pytest-xdist
.)
Full example
Configuration:
[tool.cibuildwheel]
test-sources = ["tests", "pyproject.toml"]
test-command = "python -m pytest tests"
environment.PIP_ONLY_BINARY = "numpy"
environment.PIP_PREFER_BINARY = "1"
ios.test-groups = ["test"]
ios.xbuild-tools = ["cmake", "ninja"]
ios.environment.PIP_EXTRA_INDEX_URL = "https://pypi.anaconda.org/beeware/simple"
GitHub Actions:
jobs:
build-ios:
name: iOS wheel
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- run: brew upgrade cmake
- uses: pypa/cibuildwheel@v3.0
env:
CIBW_PLATFORM: ios
CIBW_ARCHS: all
Android support is coming in cibuildwheel 3.1. Android will have two architectures and can be built from linux or macOS, though there are interesting tradeoffs on each.
Enables: GraalPy, Python 3.14, free-threading, PyPy (3.0 and 2.22)
We introduced a new enable
system in cibuildwheel 2.22, and it’s now fully
replacing the specialized CIBW_PRERELEASE_PYTHONS
and
CIBW_FREE_THREADED_SUPPORT
variables with a unified system. As a result, you
can now specify pre-release Pythons in your pyproject.toml if you want to; but
we’ve added special handling for this to make it easy to use both. If you
specify:
[tool.cibuildwheel]
enable = ["cpython-freethreading", "pypy"]
in your pyproject.toml
, then build cibuildwheel with
CIBW_ENABLE=cpython-prerelease
, all of those will be enabled; the environment
variable will only extend the static configuration. If you need to avoid certain
wheels dynamically, use CIBW_SKIP
or adjust CIBW_BUILD
; the enables simply
indicate you are aware of these special cases and are willing to work with them
in your selectors.
Selector: pypy
and pypy-eol
These new selectors mean you now opt-in to PyPy. They are:
pypy
: PyPy 3.10 and PyPy 3.11pypy-eol
: PyPy 3.8 and PyPy 3.9
Most packages that support PyPy have to make at least a little effort to do so,
so this makes it a little easier to build with cibuildwheel out-of-the box. Most
importantly, the pypy-eol
category means it’s much easier to avoid building
for versions of PyPy that no longer are supported and have unfixed bugs (looking
at you, PyPy 3.8). Most packages that support PyPy do not need to build for
end-of-life versions. The requires-python
setting is not PyPy aware, so this
should really help packages that want to build for supported versions.
Selector: graalpy
We support a new interpreter: GraalPy, which is built on GraalVM. If you are using a binding tool, you need to make sure that tool supports GraalPy; a pybind11 3.0 release candidate is required, for example. There are several caveats:
- GraalPy 24.2 on Windows is buggy, you might need to skip tests or skip it entirely. I get a pytest missing module crash.
- GraalPy identifiers use
py<version>_<graalpy_version>
, which is our first identifier like that, though it could be used on others in the future if there are ever more than one ABI per Python version. - There are some GraalPy wheels for NumPy in
"https://www.graalvm.org/python/wheels/"
. For some reason, there is no “simple” suffix on this index. Some combinations are missing, though, like ARM manylinux and Intel macOS. - UV does support GraalPy, and is much faster than pip running on GraalPy.
Here’s an example configuration (using uv, so remember to install uv first):
[tool.cibuildwheel]
build-frontend = "build[uv]"
test-command = "pytest tests"
test-skip = [
"gp311_242-macosx_x86_64",
"gp311_242-manylinux_aarch64",
"gp311_242-win*",
]
environment.UV_ONLY_BINARY = "numpy"
environment.UV_PREFER_BINARY = "1"
[[tool.cibuildwheel.overrides]]
select = ["gp*"]
inherit.environment = "append"
environment.UV_INDEX = "https://www.graalvm.org/python/wheels/"
environment.UV_INDEX_STRATEGY = "unsafe-best-match"
Note: if you want to test on GraalPy on GitHub Actions without cibuildwheel, in
your normal test workflow, setup-python
support GraalPy except on Windows.
Other Selectors
The other selectors are:
cpython-prerelease
: activates Python 3.14. Reminder that it will not be ABI stable until RC 1.cpython-freethreading
: activates Python 3.13t (and 3.14t if combined withcpython-prerelease
cpython-experimental-riscv64
: A very experimental build that can’t be uploaded to PyPI yet and requires custom images.pyodide-prerelease
: Enables the unreleased 0.28 Pyodide’s Python 3.13. These wheels may not work with the final release of Pyodide 0.28.all
: enables all selectors.
As with the old options or the --platform/CIBW_PLATFORM
setting, you don’t
need bother with them if you are using --only
to target a specific selector.
Platform default changes and removals (3.0)
Time marches on, and the announced changes to defaults have happened on
schedule. manylinux2014
is no longer the default, with manylinux_2_28
taking
its place. We’ve also removed the old shortcuts for EoL images manylinux1
,
manylinux2010
, manylinux_2_24
, and musllinux_1_1
; if you need to use
these, you must specify the full image URL (and be warned, some of the older
tags have been deleted to save space on quay.io; the most recent tags are still
available). The new images don’t have 32-bit versions, so we’ve removed 32-bit
linux from "auto"
(the default); use archs = ["auto64", "auto32"]
to get the
full set back. We might do the same thing to 32-bit Windows in the future, too.
We’ve also dropped Python 3.6 and Python 3.7 support; these have also been removed from the manylinux images following the timeline announced a year ago. You’ll have to use a cibuildwheel 2.x to build for these EoL Python (reminder that even 3.8 is EoL!).
We now use build
by default instead of pip
, which was noted for several
years. pip
(and build[uv]
) remain available, and we are working on a native
uv
build-frontend (should be workspace-aware) in the future.
Setuptools and wheel are no longer installed in environments by default,
matching later Pythons. Use pyproject.toml
or specify build dependencies
manually.
We’ve also dropped support for Appveyor, as they weren’t able to work with us to run our CI. It might work there, but we can’t be sure anymore.
Pyodide
Pyodide has seen significant updates; we no longer require the host Python (the
one you run cibuildwheel with) to be Python 3.12; instead we use
python-build-standalone to download an appropriate version. This was required
for our new experimental support for Pyodide’s 3.13 wheels; you can’t run a
program from 3.12 and 3.13 simultaneously! We also support selecting a specific
version of pyodide with pyodide-version
(you need the enable
option to
select one that has a newer Python identifier, though). This is experimental
while we work on a more general system to customize (and maybe add!) selectors.
And, of course, there’s the aforementioned selector for beta Pyodide (Python
3.13).
Other 3.0
We now require the host Python (the one you use to run cibuildwheel) to be
Python 3.11 or newer. All CI platforms have this version or greater available,
and it dramatically simplifies our maintenance (and testing) to not have to
maintain old versions of host Python. As a side effect, we have the extra leeway
now to test on pre-release 3.14 as host Python too. If you use 3.14, try running
cibuildwheel --help
and enjoy the new colorful output! You can see this in our
docs, too.
We’ve dramatically reworked our docs. Now we have many new and more focused pages, The TOML configuration comes first, with badges showing all the possible ways to set this, also including command line options. And now we even build our docs with Python 3.14 to get color output for the help display.
We also have one more new option, test-environment
, which allows you to
specify test-only environment variables.
You can also specify dependency-versions
inline now.
The Scientific Python nightly wheels (any version)
One sort-of related change is the development of SPEC 4, which creates a nightly wheel repository for scientific Python packages. Many core packages, like NumPy, are publishing wheels there. Besides getting the latest development copies of packages, this is also a fantastic way to get access to new platforms that haven’t made it into a full release yet. During PyCon 2025, a few days after CPython 3.14 beta 1 was released, we released cibuildwheel 3.0 beta 1 with Python 3.14 support, and worked with NumPy to get 3.14 wheels into the nightlies. This is the earliest we’ve ever been able to build against NumPy on a pre-release Python!
To use them, you need something like this:
[tool.cibuildwheel]
environment.PIP_ONLY_BINARY = "numpy"
environment.PIP_PREFER_BINARY = "1"
[[tool.cibuildwheel.overrides]]
select = ["cp314*"]
inherit.environment = "append"
environment.PIP_EXTRA_INDEX_URL = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"
environment.PIP_PRERELEASE = "allow"
[tool.cibuildwheel]
environment.UV_ONLY_BINARY = "numpy"
environment.UV_PREFER_BINARY = "1"
[[tool.cibuildwheel.overrides]]
select = ["cp314*"]
inherit.environment = "append"
environment.UV_INDEX = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple/"
environment.UV_INDEX_STRATEGY = "unsafe-best-match"
environment.UV_PRERELEASE = "allow"
With uv, you can also configure this, but I believe it currently only affects the high-level interface:
[tool.uv.sources]
numpy = { index = "scientific_python", marker = "python_version >='3.14'"}
[[tool.uv.index]]
name = "scientific_python"
url = "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple"
Dependency Groups (2.22)
Following the acceptance of dependency-groups
PEP 735, we added test-groups
, which can
be used like this:
[dependency-groups]
test = ["pytest"]
dev = [{ include-group = "test" }]
[tool.cibuildwheel]
test-command = "pytest {project}/tests"
test-groups = ["test"]
This is preferable to using extras (test-extras
), because it avoids exposing
test dependencies as public package metadata, has native nesting support, and if
you specify a dev
group, you can get out-of-the-box uv run
support, too!
(Note: currently, we use the dependency-groups
package to parse these, though
pip and uv now natively support a --group
option).
Inheriting overrides improvement (2.21)
The inherit feature, featured in my last post, got a fix in cibuildwheel 2.21 to
work much better with config-settings
, with keys overriding old keys, rather
than extending them as a list (?!).
Upcoming features
Android support is being worked on! It requires changes to CPython’s
android.py
built-in script too, so missed the 3.0 deadline, but should make it
into cibuildwheel 3.1.
We expect to support uv
natively as a frontend (in addition to build[uv]
).
This should bring support for uv’s workspaces, in theory.
We might be able to provide higher quality pass-through of config-settings to
build
in the future, based on a proposed addition to build.
Update now!
- uses: pypa/cibuildwheel@v3.0
If you use pybind11, you’ll need a pybind11 3.0 release candidate to support
Python 3.14 or GraalPy. iOS might work with 2.x, but the 3.0 RCs are tested
against iOS too. For any binding library including pybind11 and mypyc, there’s a
bug that will be fixed in the next Python 3.14 beta (beta 3) with setting
__dict__
, but it’s pretty rare to run into it in practice (except maybe in
pickling).
The latest versions of scikit-build-core support all these, as well.
Here’s a quick checklist to follow when updating to 3.0:
- Make sure you run cibuildwheel with 3.11+, or you won’t get the upgrade.
- Do you need PyPy? Add
"pypy"
to your enables. If you need EoL PyPy (3.8, 3.9), also include"pypy-eol"
. - Do you need 32-bit linux? Add
auto32
to your archs. - Replace
free-threaded-support = true
withenable = ["cpython-freethreading"]
(orCIBW_FREETHREADED_SUPPORT: 1
withCIBW_ENABLE: cpython-freethreading
). - Replace
CIBW_PRERELEASE_PYTHONS: 1
withCIBW_ENABLE: cpython-prerelease
orenable = ["cpython-prerelease"]
. - Verify you are happy with
manylinux_2_28
, or set the image explicitly. Old versions (2010 or earlier,musllinux_1_1
) must be fully specified images.
If you still need CPython 3.7 or 3.6, you’ll need to have a cibuildwheel 2.x job for those.
Once you’ve upgraded, consider adding iOS, GraalPy, or 3.14 beta wheels!