Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
9afc80c
Initial support for abi3t, depends on unreleased PyO3 features.
ngoldbaum Mar 31, 2026
1219225
adjust for upstream changes
ngoldbaum May 11, 2026
d84a65b
Merge branch 'main' into abi3t
ngoldbaum May 18, 2026
419d900
Add new tests for abi3t support
ngoldbaum May 18, 2026
cc2f87b
lint
ngoldbaum May 18, 2026
ee98a08
test on 3.15
ngoldbaum May 18, 2026
d8220a2
fix clippy
ngoldbaum May 18, 2026
442eae9
revert ee98a088c8c185c07c7ea2f326a256e43e7e5fa5
ngoldbaum May 18, 2026
5f2e1cb
cargo fmt
ngoldbaum May 18, 2026
8ba824f
Merge branch 'main' into abi3t
ngoldbaum May 25, 2026
80867fd
generate target_abi option for pyo3 config files, remove is_abi3_for_…
ngoldbaum May 25, 2026
68a4bfe
cargo nextest almost completely passes on Mac 3.15t!
ngoldbaum May 26, 2026
cdc4dc0
add 3.15 and 3.15t CI
ngoldbaum May 26, 2026
4e946a0
fix sdist test
ngoldbaum May 26, 2026
b5831bc
cargo fmt and clippy
ngoldbaum May 26, 2026
1d08385
fix incorrect namespace
ngoldbaum May 26, 2026
b3e0275
remove dbg uses
ngoldbaum May 26, 2026
33b0c00
update pyo3 dependency
ngoldbaum May 26, 2026
fb55c07
skip pyo3_no_extension_module on 3.15 and newer
ngoldbaum May 26, 2026
e0cd656
update pyo3 dependency
ngoldbaum May 26, 2026
02d87ad
remove local dep
ngoldbaum May 26, 2026
52dd9c6
update pyo3 depenency
ngoldbaum May 27, 2026
32e18a4
Remove 3.13t from manylinux image in cross-compilation tests
ngoldbaum May 27, 2026
2957518
fix pre-commit
ngoldbaum May 27, 2026
0f6eeb5
Drop python 3.13t builds since they're unsupported by PyO3
ngoldbaum May 27, 2026
51de93b
update tests for dropping 3.13t support
ngoldbaum May 27, 2026
e2af997
Update PyO3 dependency in test crates
ngoldbaum May 28, 2026
8aae82c
Merge branch 'main' into abi3t
ngoldbaum May 28, 2026
5b1b364
Merge branch 'main' into abi3t
ngoldbaum Jun 1, 2026
78f49aa
cargo fmt
ngoldbaum Jun 1, 2026
d0d650e
drop 3.14 and 3.14t CI
ngoldbaum Jun 1, 2026
56ce3b8
update pyo3 in test crates
ngoldbaum Jun 1, 2026
5b2c4d0
Disable PIP caching to work around Windows pip cache race
ngoldbaum Jun 2, 2026
08fc257
depend on upstream pyo3
ngoldbaum Jun 3, 2026
585e754
Merge branch 'main' into abi3t
ngoldbaum Jun 3, 2026
7b940d2
Expand stable ABI binding docs to discuss abi3t support
ngoldbaum Jun 3, 2026
517b820
Update for PyO3 0.29 release
ngoldbaum Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ jobs:
- windows-11-arm
python-version:
- "3.9"
- "3.14"
- "3.14t"
- "3.15-dev"
- "3.15t-dev"
- "pypy3.11"
exclude:
# TODO: Tests are getting stuck
Expand All @@ -66,6 +66,7 @@ jobs:
runs-on: ${{ matrix.os }}
env:
RUST_BACKTRACE: "1"
PIP_NO_CACHE_DIR: "1"
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
steps:
Expand Down Expand Up @@ -425,6 +426,7 @@ jobs:
- name: Build wheels
run: |
set -ex

# Use bundled sysconfig
bin/maturin build -i ${{ matrix.platform.python }} --release --out dist --target ${{ matrix.platform.target }} -m test-crates/pyo3-mixed/Cargo.toml ${{ matrix.platform.extra-args }}

Expand Down
28 changes: 20 additions & 8 deletions guide/src/bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,30 @@ maturin automatically detects pyo3 bindings when it's added as a dependency in `

### `Py_LIMITED_API`/abi3

pyo3 bindings has `Py_LIMITED_API`/abi3 support, enable the `abi3` feature of the `pyo3` crate to use it:
The pyo3 bindings supports the Python stable ABI (`Py_LIMITED_API`/abi3/abi3t).
You can use it by enabling `"abi3"` and/or `"abi3t"` features. We suggest
picking a minimum supported Python version for both features:

```toml
pyo3 = { version = "0.28.3", features = ["abi3"] }
pyo3 = { version = "0.29.0", features = ["abi3-py310", "abi3t-py315"] }
```

You may additionally specify a minimum Python version by using the `abi3-pyXX`
format for the pyo3 features, where `XX` is corresponds to a Python version.
For example `abi3-py37` will indicate a minimum Python version of 3.7.

> **Note**: Read more about abi3 support in [pyo3's
> documentation](https://pyo3.rs/latest/building-and-distribution#py_limited_apiabi3).
When selecting a specific interpreter to build against, this will produce an
`abi3-py310` wheel for Python 3.14 and older and an `abi3.abi3t-py315` wheel on
Python 3.15 and newer. If you build with `--find-interpreters`, maturin will
produce an `abi3-py310`, `cp314t-cp314` and an `abi3.abi3t-py315` wheel. These
three wheels cover all non-EOL and non-experimental builds of CPython. Other
python implementations like RustPython may also target the abi3t ABI in the
future.

An `abi3-py310` wheel supports all GIL-enabled Python
versions from Python 3.10 to Python 3.14 and the `abi3.abi3t` wheel supports
Python 3.15 and all newer versions of CPython.

> **Note**: Read more about stable ABI support in [pyo3's
> documentation](https://pyo3.rs/latest/building-and-distribution#py_limited_apiabi3abi3t). You
> can read more about using abi3 and abi3t wheels simultaneously in the
> HOWTO guide on migrating to abi3t: https://docs.python.org/3.16/howto/abi3t-migration.html#why-do-this.

### Cross Compiling

Expand Down
2 changes: 1 addition & 1 deletion guide/src/distribution.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ or providing any Windows Python library files.

```toml
[dependencies]
pyo3 = { version = "0.28.3", features = ["generate-import-lib"] }
pyo3 = { version = "0.29.0", features = ["generate-import-lib"] }
```

It uses an external [`python3-dll-a`](https://docs.rs/python3-dll-a/latest/python3_dll_a/) crate to
Expand Down
2 changes: 1 addition & 1 deletion guide/src/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ crate-type = ["cdylib"]
rand = "0.9.0"

[dependencies.pyo3]
version = "0.28.3"
version = "0.29.0"
# "abi3-py38" tells pyo3 (and maturin) to build using the stable ABI with minimum Python version 3.8
features = ["abi3-py38"]
```
Expand Down
3 changes: 3 additions & 0 deletions src/binding_generator/pyo3_binding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct Pyo3BindingGenerator<'a> {

enum BindingType<'a> {
Abi3(Option<&'a PythonInterpreter>),
Abi3t(Option<&'a PythonInterpreter>),
VersionSpecific(&'a PythonInterpreter),
}

Expand All @@ -49,6 +50,7 @@ impl<'a> Pyo3BindingGenerator<'a> {
let binding_type = match stable_abi {
Some(kind) => match kind {
StableAbiKind::Abi3 => BindingType::Abi3(interpreter),
StableAbiKind::Abi3t => BindingType::Abi3t(interpreter),
},
None => {
let interpreter = interpreter.ok_or_else(|| {
Expand Down Expand Up @@ -101,6 +103,7 @@ impl<'a> BindingGenerator for Pyo3BindingGenerator<'a> {

let so_filename = match self.binding_type {
BindingType::Abi3(interpreter) => ext_suffix(target, interpreter, ext_name, "abi3"),
BindingType::Abi3t(interpreter) => ext_suffix(target, interpreter, ext_name, "abi3t"),
BindingType::VersionSpecific(interpreter) => interpreter.get_library_name(ext_name),
};
let artifact_target = ArtifactTarget::ExtensionModule(module.join(so_filename));
Expand Down
13 changes: 6 additions & 7 deletions src/bridge/detection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const PYO3_BINDING_CRATES: [PyO3Crate; 2] = [PyO3Crate::PyO3Ffi, PyO3Crate::PyO3
/// is forced; otherwise it's auto-detected from dependencies and target types.
///
/// Conditional pyo3/pyo3-ffi features from pyproject.toml are excluded from
/// abi3 inference here. Use [`upgrade_bridge_abi3`] after interpreter resolution
/// abi3 inference here. Use [`upgrade_bridge_stable_abi`] after interpreter resolution
/// to evaluate them.
pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<BridgeModel> {
let no_extra_features = HashMap::new();
Expand Down Expand Up @@ -147,7 +147,7 @@ pub fn find_bridge(cargo_metadata: &Metadata, bridge: Option<&str>) -> Result<Br
/// This is the second phase of bridge detection: [`find_bridge`] excludes
/// conditional features, then after interpreter resolution this function
/// re-checks whether any conditional abi3 feature applies.
pub fn upgrade_bridge_abi3(
pub fn upgrade_bridge_stable_abi(
bridge: BridgeModel,
cargo_metadata: &Metadata,
pyproject: Option<&PyProjectToml>,
Expand Down Expand Up @@ -223,11 +223,11 @@ fn has_stable_abi(
deps: &HashMap<&str, &Node>,
extra_features: &HashMap<&str, Vec<String>>,
) -> Result<Option<StableAbi>> {
let abi3 = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)?;
if abi3.is_some() {
return Ok(abi3);
let abi3t = has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3t)?;
if abi3t.is_some() {
return Ok(abi3t);
}
Ok(None)
has_stable_abi_from_kind(deps, extra_features, StableAbiKind::Abi3)
}

/// pyo3 supports building stable abi wheels if the unstable-api feature is not selected
Expand All @@ -240,7 +240,6 @@ fn has_stable_abi_from_kind(
let lib = lib.as_str();
if let Some(&pyo3_crate) = deps.get(lib) {
let extra = extra_features.get(lib);
// Find the minimal abi3 python version. If there is none, abi3 hasn't been selected
// Find the minimal stable abi python version. If there is none, stable abi hasn't been selected
// This parses abi3-py{major}{minor} and returns the minimal (major, minor) tuple
let all_features: Vec<&str> = pyo3_crate
Expand Down
87 changes: 74 additions & 13 deletions src/bridge/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
mod detection;

pub use detection::{find_bridge, has_windows_import_lib_support, upgrade_bridge_abi3};
pub use detection::{find_bridge, has_windows_import_lib_support, upgrade_bridge_stable_abi};

use std::{fmt, str::FromStr};

Expand Down Expand Up @@ -132,6 +132,23 @@ impl StableAbi {
version: StableAbiVersion::Version(major, minor),
}
}

/// Create a StableAbi instance from a known abi3t version
pub fn from_abi3t_version(major: u8, minor: u8) -> StableAbi {
StableAbi {
kind: StableAbiKind::Abi3t,
version: StableAbiVersion::Version(major, minor),
}
}
}

impl std::fmt::Display for StableAbi {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self.kind {
StableAbiKind::Abi3 => write!(f, "abi3"),
StableAbiKind::Abi3t => write!(f, "abi3t"),
}
}
}

/// Python version to use as the abi3/abi3t target.
Expand All @@ -146,7 +163,7 @@ pub enum StableAbiVersion {
}

impl StableAbiVersion {
/// Convert `StableAbiVersion` into an Option, where CurrentPython maps None
/// Convert `StableAbiVersion` into an Option, where CurrentPython maps to None
pub fn min_version(&self) -> Option<(u8, u8)> {
match self {
StableAbiVersion::CurrentPython => None,
Expand All @@ -160,12 +177,15 @@ impl StableAbiVersion {
pub enum StableAbiKind {
/// The original stable ABI, supporting Python 3.2 and up
Abi3,
/// The free-threaded stable ABI, supporting Python 3.15 and up
Abi3t,
}

impl fmt::Display for StableAbiKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StableAbiKind::Abi3 => write!(f, "abi3"),
StableAbiKind::Abi3t => write!(f, "abi3t"),
}
}
}
Expand All @@ -175,6 +195,7 @@ impl StableAbiKind {
pub fn wheel_tag(&self) -> &str {
match self {
StableAbiKind::Abi3 => "abi3",
StableAbiKind::Abi3t => "abi3.abi3t",
}
}
}
Expand Down Expand Up @@ -340,32 +361,36 @@ impl BridgeModel {
///
/// This is a project-level check — it does not consider whether a particular
/// interpreter meets the abi3 minimum version. For per‑interpreter checks
/// use [`is_abi3_for_interpreter`](Self::is_abi3_for_interpreter).
/// use [`is_stable_abi_for_interpreter`](Self::is_stable_abi_for_interpreter).
pub fn has_stable_abi(&self) -> bool {
self.pyo3()
.and_then(|pyo3| pyo3.stable_abi.as_ref())
.is_some()
}

/// Check whether abi3 should be enabled for a specific interpreter.
/// Check whether an abi3 or abi3t build should be enabled for a specific interpreter.
///
/// Returns `true` only when the bridge model has stable abi support **and**
/// the given interpreter supports the stable ABI **and** meets the abi3
/// minimum version. Version‑specific fallback builds (e.g. Python 3.10 when
/// abi3 targets ≥ 3.11) return `false` so that `Py_LIMITED_API` is not
/// defined and interpreter‑specific linker names are used.
pub fn is_abi3_for_interpreter(&self, interpreter: &PythonInterpreter) -> bool {
if !interpreter.has_stable_api() {
return false;
}

pub fn is_stable_abi_for_interpreter(&self, interpreter: &PythonInterpreter) -> bool {
self.pyo3()
.and_then(|pyo3| pyo3.stable_abi.as_ref())
.is_some_and(|stable_abi| match stable_abi.version.min_version() {
Some((major, minor)) => {
(interpreter.major as u8, interpreter.minor as u8) >= (major, minor)
.is_some_and(|stable_abi| {
if !interpreter.has_stable_api(stable_abi.kind) {
return false;
}
if matches!(stable_abi.kind, StableAbiKind::Abi3) && interpreter.gil_disabled {
return false;
};
match stable_abi.version.min_version() {
Some((major, minor)) => {
(interpreter.major as u8, interpreter.minor as u8) >= (major, minor)
}
None => true, // CurrentPython → compatible when stable ABI is supported
}
None => true, // CurrentPython → compatible when stable ABI is supported
})
}

Expand All @@ -392,3 +417,39 @@ impl fmt::Display for BridgeModel {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn stable_abi_kind_display() {
assert_eq!(StableAbiKind::Abi3.to_string(), "abi3");
assert_eq!(StableAbiKind::Abi3t.to_string(), "abi3t");
}

#[test]
fn stable_abi_kind_wheel_tag() {
assert_eq!(StableAbiKind::Abi3.wheel_tag(), "abi3");
// abi3t wheels are also importable on abi3-capable interpreters, so the
// wheel tag is the compressed form `abi3.abi3t`.
assert_eq!(StableAbiKind::Abi3t.wheel_tag(), "abi3.abi3t");
}

#[test]
fn stable_abi_display() {
assert_eq!(StableAbi::from_abi3_version(3, 7).to_string(), "abi3");
assert_eq!(StableAbi::from_abi3t_version(3, 15).to_string(), "abi3t");
}

#[test]
fn stable_abi_constructors() {
let abi3 = StableAbi::from_abi3_version(3, 9);
assert_eq!(abi3.kind, StableAbiKind::Abi3);
assert_eq!(abi3.version, StableAbiVersion::Version(3, 9));

let abi3t = StableAbi::from_abi3t_version(3, 15);
assert_eq!(abi3t.kind, StableAbiKind::Abi3t);
assert_eq!(abi3t.version, StableAbiVersion::Version(3, 15));
}
}
26 changes: 19 additions & 7 deletions src/build_context/builder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use crate::auditwheel::{AuditWheelMode, PlatformTag};
use crate::bridge::{find_bridge, has_windows_import_lib_support, upgrade_bridge_abi3};
use crate::bridge::{
StableAbiVersion, find_bridge, has_windows_import_lib_support, upgrade_bridge_stable_abi,
};
use crate::build_options::{BuildOptions, TargetTriple};
use crate::compile::filter_cargo_targets;
use crate::metadata::Metadata24;
Expand Down Expand Up @@ -129,7 +131,7 @@ impl BuildContextBuilder {
});

// Detect bridge without conditional pyo3 features — those are
// evaluated after interpreter resolution via upgrade_bridge_abi3.
// evaluated after interpreter resolution via upgrade_bridge_stable_abi.
let bridge = find_bridge(&cargo_metadata, bindings)?;

if !bridge.is_bin() && project_layout.extension_name.contains('-') {
Expand Down Expand Up @@ -163,16 +165,26 @@ impl BuildContextBuilder {
// (e.g. abi3-py311 gated on python-version>=3.11) match any
// of the resolved interpreters.
let bridge = if has_conditional_pyo3_features {
upgrade_bridge_abi3(bridge, &cargo_metadata, pyproject, &interpreter)?
upgrade_bridge_stable_abi(bridge, &cargo_metadata, pyproject, &interpreter)?
} else {
bridge
};
debug!("Resolved bridge model: {:?}", bridge);
if let Some(stable_abi) = bridge.pyo3().and_then(|p| p.stable_abi.as_ref()) {
eprintln!(
"🔗 Found {bridge} bindings with {} support",
stable_abi.kind
);
match stable_abi.version {
StableAbiVersion::Version(major, minor) => {
eprintln!(
"🔗 Found {bridge} bindings with {}-py{}.{} support",
stable_abi.kind, major, minor
);
}
StableAbiVersion::CurrentPython => {
eprintln!(
"🔗 Found {bridge} bindings with {} support",
stable_abi.kind
);
}
}
} else {
eprintln!("🔗 Found {bridge} bindings");
}
Expand Down
Loading
Loading