Skip to content

add PythonABI and new builder structs for pyo3-build-config InterpreterConfig#5924

Open
ngoldbaum wants to merge 20 commits intoPyO3:mainfrom
ngoldbaum:abi-tag-refactor
Open

add PythonABI and new builder structs for pyo3-build-config InterpreterConfig#5924
ngoldbaum wants to merge 20 commits intoPyO3:mainfrom
ngoldbaum:abi-tag-refactor

Conversation

@ngoldbaum
Copy link
Copy Markdown
Contributor

@ngoldbaum ngoldbaum commented Mar 30, 2026

Towards #5786.

Refactor pyo3_build_config::impl_::InterpreterConfig to use an enum to represent the kinds of stable ABI instead of boolean abi3 flag. Also replace names that contain "abi3" with "stable_abi".

This is extracted from a branch that enables abi3t builds and Python 3.15 stable ABI support, where I add a third enum variant to represent abi3t. My goal here is to make upstreaming that change simpler. PEP 803 was accepted over the weekend so a new ABI is definitely happening.

I also personally find the enum clearer to understand and easier to read code that uses it instead of the boolean flag.

@ngoldbaum ngoldbaum force-pushed the abi-tag-refactor branch 2 times, most recently from 2e92e57 to 4f2177f Compare March 30, 2026 18:37
messense added a commit to PyO3/maturin that referenced this pull request Mar 31, 2026
#3110)

Towards #3064.

This is purely refactoring, there should be no functional changes as a
result of this.

Currently the build metadata special-cases ABI3 builds or more generally
assumes stable ABI builds and ABI3 builds are the same thing. With PEP
803 and the new abi3t ABI in Python 3.15, that is no longer the case.

This replaces the old `ABI3Version` enum with a new struct combining two
enums:

```rust
/// struct describing ABI layout to use for build
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StableAbi {
    /// The "kind" of stable ABI. Either abi3 or abi3t currently.
    pub kind: StableAbiKind,
    /// The minimum Python version to build for.
    pub version: StableAbiVersion,
}

/// Python version to use as the abi3/abi3t target.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StableAbiVersion {
    /// Stable ABI wheels will have a minimum Python version matching the
    /// version of the current Python interpreter
    CurrentPython,
    /// Stable ABI wheels will have a fixed user-specified minimum Python
    /// version
    Version(u8, u8),
}

/// The "kind" of stable ABI. Either abi3 or abi3t currently.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StableAbiKind {
    /// The original stable ABI, supporting Python 3.2 and up
    Abi3,
}
```

`StableAbiVersion` is just the old `Abi3Version` enum renamed since the
concept of a minimum supported version is shared by abi3t.

I have [a
branch](main...ngoldbaum:maturin:abi3t)
that adds an `Abi3t` variant for `StableAbiKind`. My goal with this PR
is to make reviewing the subsequent PR adding abi3t support easier.

Also see PyO3/pyo3#5924 where I made a similar
change in PyO3. Here in Maturin I needed different types but in
principle I could make the two implementations use shared code. I'm not
sure if that's actually useful for anything in practice.

---------

Co-authored-by: messense <messense@icloud.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, some hazy thoughts here, not sure if I'm being helpful throwing these out there.

Comment thread pyo3-build-config/src/impl_.rs
Comment thread pyo3-build-config/src/impl_.rs Outdated
@ngoldbaum ngoldbaum changed the title add CPythonABI enum for pyo3-build-config InterpreterConfig add PythonABI struct and use it for pyo3-build-config InterpreterConfig Apr 15, 2026
@ngoldbaum ngoldbaum force-pushed the abi-tag-refactor branch 2 times, most recently from 51ff27b to dc5c1d1 Compare April 17, 2026 21:18
Comment thread pyo3-build-config/src/impl_.rs
Copy link
Copy Markdown
Member

@Icxolu Icxolu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't worked too much inside the build code, so I'm not sure that I can help much here, but I gave this a brief read and left some comments.

I do like the builder pattern. I think it's nice to have a different type between the abi configuring phase and the usage phase (even tho it does not protect us from forgetting to configure something)

Comment thread pyo3-build-config/src/impl_.rs Outdated
Comment thread pyo3-build-config/src/impl_.rs Outdated
Comment thread pyo3-build-config/src/impl_.rs Outdated
Copy link
Copy Markdown
Member

@davidhewitt davidhewitt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have taken a look through some parts, I think I have a couple of pieces that I'd like clarifying in this PR:

  • I can't decide if we want to separate "host version" from target abi version. That seemed like it might help in #5960, but maybe it adds complexity for no practical benefit.
  • I think it'd be helpful to introduce the possibility to configure for abi3t in this PR through a config file, even if the full end-to-end build with abi3t is not done here (we could maybe just halt the build if abi3t is selected for now). I think that'd make it easier to see the full end state possible states we're heading towards, plus help understand where "stable abi" vs "abi3 and abi3t" are the right names.

Comment thread pyo3-build-config/src/impl_.rs
Comment thread pyo3-build-config/src/impl_.rs
Comment thread pyo3-build-config/src/impl_.rs
@ngoldbaum
Copy link
Copy Markdown
Contributor Author

ngoldbaum commented Apr 24, 2026

I just pushed a big commit that restores the version and implementation fields on InterpreterConfig, deprecates the abi3 field, and adds a new target_abi field which is a PythonAbi struct.

I also added a new InterpreterConfigBuilder struct to try to contain some of the boilerplate.

It ends up being an even bigger refactor! But I think we're getting close now.

I also didn't end up adding abi3t yet because this got kinda big. I'm going to try rebasing #5807 on top of this next week and then I can add a stub abi3t implementation here.

In the meantime, I'd very much appreciate feedback on the new approach.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is out of date now and needs to be updated

@ngoldbaum ngoldbaum changed the title add PythonABI struct and use it for pyo3-build-config InterpreterConfig add PythonABI and new builder structs for pyo3-build-config InterpreterConfig Apr 24, 2026
@ngoldbaum ngoldbaum added the CI-no-fail-fast If one job fails, allow the rest to keep testing label Apr 28, 2026
@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented Apr 30, 2026

Merging this PR will improve performance by 13.8%

⚡ 1 improved benchmark
✅ 104 untouched benchmarks
⏩ 1 skipped benchmark1

Performance Changes

Benchmark BASE HEAD Efficiency
bench_pyclass_create 4.6 µs 4 µs +13.8%

Comparing ngoldbaum:abi-tag-refactor (3a21665) with main (cf4d883)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@ngoldbaum
Copy link
Copy Markdown
Contributor Author

@davidhewitt @Icxolu this is ready for another pass.

}
}

/// The "kind" of stable ABI. Either abi3 or abi3t currently.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

out of date

self.target_abi.is_none(),
"Target ABI already set to {}",
target_abi
);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should probably have a similar check for all the builder functions

self.version,
version.minor,
);
/// Whether the ABI is for the GIL-enabled or free-threaded build.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy/paste error from GilUsed


/// Checks if `abi3` or any of the `abi3-py3*` features is enabled for the PyO3 crate.
///
/// Must be called from a PyO3 crate build script.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shouldn't be deleted

#[cfg_attr(test, derive(Debug, PartialEq, Eq))]
#[derive(Clone, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[derive(Debug, Clone, Default)]
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be reverted

Copy link
Copy Markdown
Contributor Author

@ngoldbaum ngoldbaum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked Claude to do a review pass and it spotted some things, along with this comment, which I agree with:

There are now several near-equivalent expressions:

  • target_abi.kind.is_free_threaded() (the canonical predicate)
  • flags.0.contains(&BuildFlag::Py_GIL_DISABLED)
  • the gil_disabled boolean threaded through from_interpreter and from_sysconfigdata
  • cross_compile_config.abiflags.as_deref() == Some("t")

The PR doesn't create this fragmentation, but it does add the new canonical predicate without unifying the
others. Worth a follow-up issue: every path should converge on target_abi.kind.is_free_threaded() after
construction, with Py_GIL_DISABLED in build_flags purely a downstream consequence.

Comment on lines +898 to +912
let build_flags = if build_flags.0.contains(&BuildFlag::Py_GIL_DISABLED) {
if let Some(target_abi) = self.target_abi {
if !target_abi.kind.is_free_threaded() {
warn!(
"build_flags contains Py_GIL_DISABLED but target ABI '{target_abi}' is not free-threaded"
);
}
}
build_flags
} else if let Some(target_abi) = self.target_abi {
let mut flags = build_flags.clone();
if target_abi.kind.is_free_threaded() {
flags.0.insert(BuildFlag::Py_GIL_DISABLED);
}
flags
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On reflection, it probably makes sense to move these two branches into finalize when the final state is known.

let parts: Vec<&str> = value.split("-").collect();
let implementation = parts[0].parse()?;
let kind = parts[1].parse()?;
let version: PythonVersion = parts[2].parse()?;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should use splitn(3, '-') and ok_or to avoid panics

target_abi: self
.target_abi
.unwrap_or(PythonAbiBuilder::new(self.implementation, self.version).finalize()),
abi3: false,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be something like matches!(target_abi.kind, PythonAbiKind::Stable(StableAbi::Abi3)) instead

@@ -470,11 +508,17 @@ print("gil_disabled", get_config_var("Py_GIL_DISABLED"))
.context("failed to parse contents of PYO3_CONFIG_FILE")?;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we care that this discards the target abi in the config file if an abi3 feature is set in the build environment?

.free_threaded()
.unwrap()
.finalize()
} else if (abi3.is_some() && abi3.unwrap()) || is_abi3() {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pattern abi3.is_some() && abi3.unwrap() should be spelled abi3 == Some(true). See also other occurrences below.

"Invalid config that sets both target_abi and abi3."
);
target_abi
} else if is_abi3t() {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should probably error if abi3 is set?

.stable_abi(StableAbi::Abi3t)
.unwrap()
.finalize()
} else if flags_contains_free_threaded {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add comment explaining that this should fire even if is_abi3() is true for backwards compatibility

let mut abi_builder = if gil_disabled {
PythonAbiBuilder::new(implementation, target_version)
} else {
// we already
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should delete this comment fragment

Comment on lines +346 to +355
let target_version = if let Some(min_version) = abi3_version {
ensure!(
min_version <= version,
"cannot set a minimum Python version {} higher than the interpreter version {} \
(the minimum Python version is implied by the abi3-py3{} feature)",
min_version,
version,
min_version.minor
);
min_version
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should get refactored into a helper function and also get used in the from_build_env implementation, which does a similar mapping from a user-supplied abi3_version parameter to an ABI3 version used for a build.

pointer_width: Option<u32>,
build_flags: Option<BuildFlags>,
suppress_build_script_link_lines: Option<bool>,
extra_build_script_lines: Option<Vec<String>>,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor but the option is unnecessary, this can default to vec![]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CI-no-fail-fast If one job fails, allow the rest to keep testing free-threading refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants