Add Python 3.13–3.15 and free-threading wheel support (#2314)
Update Python bindings to support Python 3.10–3.15, including free-threaded
3.14t and 3.15t builds.
Switch the wheel build to nanobind 3 split mode (`nanobind-backend>=1.0`),
which moves Python-version-specific runtime internals into `nanobind-backend`
so that stable ABI wheels work starting from Python 3.10 instead of 3.12 and
support PEP 803 (`abi3t`) on Python 3.15t+. This reduces the wheel matrix to
three wheels per platform architecture (`cp310-abi3`, `cp314-cp314t`, and
`cp315-abi3t`), and the `cp310-abi3` and `cp315-abi3t` wheels should be
forward-compatible with future Python releases.
Assisted-by: Gemini
Co-authored-by: dominic <510002+dmah42@users.noreply.github.com>
diff --git a/.bazelversion b/.bazelversion
index 2b0aa21..f9c71a5 100644
--- a/.bazelversion
+++ b/.bazelversion
@@ -1 +1 @@
-8.2.1
+8.5.1
diff --git a/.github/install_bazel.sh b/.github/install_bazel.sh
index a1693b8..656b9f0 100644
--- a/.github/install_bazel.sh
+++ b/.github/install_bazel.sh
@@ -4,7 +4,7 @@
arch="arm64"
fi
echo "Downloading $arch Bazel binary from GitHub releases."
- curl -L -o $HOME/bin/bazel --create-dirs "https://github.com/bazelbuild/bazel/releases/download/8.2.0/bazel-8.2.0-linux-$arch"
+ curl -L -o $HOME/bin/bazel --create-dirs "https://github.com/bazelbuild/bazel/releases/download/8.5.1/bazel-8.5.1-linux-$arch"
chmod +x $HOME/bin/bazel
else
# Bazel is installed for the correct architecture
diff --git a/.github/workflows/test_bindings.yml b/.github/workflows/test_bindings.yml
index 1bd3fb9..7f6aba3 100644
--- a/.github/workflows/test_bindings.yml
+++ b/.github/workflows/test_bindings.yml
@@ -20,7 +20,7 @@
fail-fast: false
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
- python-version: [ "3.10", "3.11", "3.12", "3.13" ]
+ python-version: [ "3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15", "3.15t" ]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -31,6 +31,7 @@
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}
+ allow-prereleases: true
- name: Install Python bindings on ${{ matrix.os }}
run: python -m pip install .
- name: Run example on ${{ matrix.os }} under Python ${{ matrix.python-version }}
diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml
index 78518c5..ac0635d 100644
--- a/.github/workflows/wheels.yml
+++ b/.github/workflows/wheels.yml
@@ -59,9 +59,10 @@
- name: Build wheels on ${{ matrix.os }} using cibuildwheel
uses: pypa/cibuildwheel@e090b81e30c4d855ea63bf4b6e59204c09a101ae # v4.2.1
env:
- CIBW_BUILD: "cp310-* cp311-* cp312-*"
+ CIBW_ENABLE: "cpython-freethreading"
+ CIBW_BUILD: "cp310-* cp314t-* cp315t-*"
CIBW_BUILD_FRONTEND: "build[uv]"
- CIBW_SKIP: "*-musllinux_*"
+ CIBW_SKIP: "cp313t-* *-musllinux_*"
CIBW_ARCHS: auto64
CIBW_BEFORE_ALL_LINUX: bash .github/install_bazel.sh
# Grab the rootless Bazel installation inside the container.
diff --git a/MODULE.bazel b/MODULE.bazel
index 159de1b..fa51298 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -7,7 +7,7 @@
bazel_dep(name = "platforms", version = "0.0.11")
bazel_dep(name = "rules_cc", version = "0.1.5")
-bazel_dep(name = "rules_python", version = "1.7.0", dev_dependency = True)
+bazel_dep(name = "rules_python", version = "2.4.0", dev_dependency = True)
bazel_dep(name = "googletest", version = "1.14.0", dev_dependency = True, repo_name = "com_google_googletest")
bazel_dep(name = "libpfm", version = "4.11.0.bcr.1")
@@ -28,6 +28,7 @@
)
python.toolchain(python_version = "3.13")
python.toolchain(python_version = "3.14")
+python.toolchain(python_version = "3.15")
pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip", dev_dependency = True)
pip.parse(
@@ -39,4 +40,4 @@
# -- bazel_dep definitions -- #
-bazel_dep(name = "nanobind_bazel", version = "2.12.0", dev_dependency = True)
+bazel_dep(name = "nanobind_bazel", version = "3.1.0.bcr.1", dev_dependency = True)
diff --git a/bindings/python/google_benchmark/BUILD b/bindings/python/google_benchmark/BUILD
index 2d49467..8009b14 100644
--- a/bindings/python/google_benchmark/BUILD
+++ b/bindings/python/google_benchmark/BUILD
@@ -4,10 +4,13 @@
py_library(
name = "google_benchmark",
srcs = ["__init__.py"],
+ data = [":_benchmark"],
+ imports = [".."],
visibility = ["//visibility:public"],
- deps = [
- ":_benchmark",
- ],
+ deps = select({
+ "@nanobind_bazel//:with_split_mode": ["@nanobind_bazel//:nanobind_backend"],
+ "//conditions:default": [],
+ }),
)
nanobind_extension(
diff --git a/docs/python_bindings.md b/docs/python_bindings.md
index d9c5d2d..718d107 100644
--- a/docs/python_bindings.md
+++ b/docs/python_bindings.md
@@ -3,7 +3,7 @@
Python bindings are available as wheels on [PyPI](https://pypi.org/project/google-benchmark/) for importing and
using Google Benchmark directly in Python.
Currently, pre-built wheels exist for macOS (both ARM64 and Intel x86), Linux x86-64 and 64-bit Windows.
-Supported Python versions are Python 3.8 - 3.12.
+Supported Python versions are Python 3.10 - 3.15, including free-threaded builds on Python 3.14t and 3.15t.
To install Google Benchmark's Python bindings, run:
diff --git a/pyproject.toml b/pyproject.toml
index 24b4f74..4cdf970 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -19,11 +19,15 @@
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
+ "Programming Language :: Python :: 3.15",
+ "Programming Language :: Python :: 3 :: Only",
+ "Programming Language :: Python :: Free Threading",
"Topic :: Software Development :: Testing",
"Topic :: System :: Benchmark",
]
-dynamic = ["readme", "version"]
+dynamic = ["dependencies", "readme", "version"]
[dependency-groups]
dev = ["pre-commit>=3.3.3"]
diff --git a/setup.py b/setup.py
index a92fa9b..a941aea 100644
--- a/setup.py
+++ b/setup.py
@@ -4,21 +4,35 @@
import re
import shutil
import sys
+import sysconfig
from collections.abc import Generator
from pathlib import Path
from typing import Any
import setuptools
-from setuptools.command import build_ext
+from setuptools.command import bdist_wheel, build_ext
IS_WINDOWS = platform.system() == "Windows"
IS_MAC = platform.system() == "Darwin"
IS_LINUX = platform.system() == "Linux"
+IS_FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
-# hardcoded SABI-related options. Requires that each Python interpreter
-# (hermetic or not) participating is of the same major-minor version.
-py_limited_api = sys.version_info >= (3, 12)
-options = {"bdist_wheel": {"py_limited_api": "cp312"}} if py_limited_api else {}
+# Build in nanobind 3 split mode targeting stable ABI for:
+# - Standard CPython 3.10+ (cp310-abi3)
+# - Free-threaded CPython 3.15t+ (cp315-abi3t, PEP 803)
+# Free-threaded CPython 3.14t has no stable ABI and is built in linked mode.
+SPLIT_MODE = not IS_FREE_THREADED or sys.version_info >= (3, 15)
+SABI_TARGET = "cp315t" if IS_FREE_THREADED else "cp310"
+if IS_WINDOWS:
+ EXT_SUFFIX = ".pyd"
+elif SPLIT_MODE:
+ EXT_SUFFIX = ".abi3t.so" if IS_FREE_THREADED else ".abi3.so"
+else:
+ EXT_SUFFIX = ".so"
+py_limited_api = SPLIT_MODE
+options = (
+ {"bdist_wheel": {"py_limited_api": "cp310"}} if not IS_FREE_THREADED else {}
+)
def is_cibuildwheel() -> bool:
@@ -72,6 +86,16 @@
self.relpath, self.target_name = stripped_target.split(":")
+class BdistWheel(bdist_wheel.bdist_wheel):
+ """Custom bdist_wheel command to handle PEP 803 abi3t wheel tags."""
+
+ def get_tag(self) -> tuple[str, str, str]:
+ python, abi, plat = super().get_tag()
+ if IS_FREE_THREADED and sys.version_info >= (3, 15):
+ return "cp315", "abi3t", plat
+ return python, abi, plat
+
+
class BuildBazelExtension(build_ext.build_ext):
"""A command that runs Bazel to build a C/C++ extension."""
@@ -107,8 +131,16 @@
f"--@rules_python//python/config_settings:python_version={python_version}",
]
- if ext.py_limited_api:
- bazel_argv += ["--@nanobind_bazel//:py-limited-api=cp312"]
+ if IS_FREE_THREADED:
+ bazel_argv.append(
+ "--@rules_python//python/config_settings:py_freethreaded=yes"
+ )
+
+ if SPLIT_MODE:
+ bazel_argv += [
+ f"--@nanobind_bazel//:py-limited-api={SABI_TARGET}",
+ "--@nanobind_bazel//:split-mode=True",
+ ]
if IS_WINDOWS:
# Link with python*.lib.
@@ -121,11 +153,6 @@
with _maybe_patch_toolchains():
self.spawn(bazel_argv)
- if IS_WINDOWS:
- suffix = ".pyd"
- else:
- suffix = ".abi3.so" if ext.py_limited_api else ".so"
-
# copy the Bazel build artifacts into setuptools' libdir,
# from where the wheel is built.
pkgname = "google_benchmark"
@@ -146,12 +173,13 @@
# we do not want the bare .so file included
# when building for ABI3, so we require a
# full and exact match on the file extension.
- if "".join(fp.suffixes) == suffix or fp.suffix == ".pyi":
+ if "".join(fp.suffixes) == EXT_SUFFIX or fp.suffix == ".pyi":
shutil.copyfile(root / fp, libdir / fp)
setuptools.setup(
- cmdclass={"build_ext": BuildBazelExtension},
+ cmdclass={"build_ext": BuildBazelExtension, "bdist_wheel": BdistWheel},
+ install_requires=["nanobind-backend>=1.0"] if SPLIT_MODE else [],
package_data={"google_benchmark": ["py.typed", "*.pyi"]},
ext_modules=[
BazelExtension(