Evaluation: PyO3 abi3t feature for wheel distribution

Status: decided — do not adopt now. Revisit when the project drops Python 3.12–3.15 and the free-threading audit (see below) is complete.

Filed as ergo task DRZBPR. PyO3 0.29 (current rust/Cargo.toml) introduces the abi3t / abi3t-py315 features, targeting PEP 803’s “truly stable” limited API. This note records whether abi3t should replace or supplement the current abi3-py312 feature.

Current state

# rust/Cargo.toml
pyo3 = { version = "^0.29", features = ["abi3-py312", "serde"] }
# pyproject.toml
requires-python = ">=3.12"
[tool.maturin]
bindings = "pyo3"
module-name = "degenbot._ffi"

CI matrix: Python 3.12, 3.13, 3.14, 3.15 (.github/workflows/*.yml).

Findings against the three checklist items

1. APIs available under abi3t vs abi3-py312

Identical Rust-visible API. PyO3 gates every limited-API code path on the single Py_LIMITED_API cfg flag (pyo3-build-config/src/impl_.rs, InterpreterConfig::to_command()), and both abi3 and abi3t set it:

match self.target_abi.kind() {
    PythonAbiKind::Stable(kind) => {
        out.push("cargo:rustc-cfg=Py_LIMITED_API".to_owned());
        if kind == StableAbi::Abi3t {
            out.push("cargo:rustc-cfg=Py_GIL_DISABLED".to_owned());
        }
    }
    // ...
}

So abi3t is not an alternate, smaller API surface than abi3. It is abi3 plus two extra constraints:

  • Py_GIL_DISABLED — the true difference. abi3t is the free-threaded (”t” = truly stable) stable ABI. abi3 (non-t) is rejected on the free-threaded build (stable_abi(StableAbi::Abi3) is skipped when gil_disabled).

  • Minimum Python 3.15. MINIMUM_SUPPORTED_VERSION_ABI3T = PythonVersion { major: 3, minor: 15 } (pyo3-build-config/src/impl_.rs), and the default_stable_abi_config helper hard-errors below it:

    Cannot target an abi3t version below 3.15

2. Does our code use any non-abi3t-compatible APIs?

No compile-time incompatibility: the crate already builds under Py_LIMITED_API today (via abi3-py312), so the limited-API surface is already in use and clean. The compatibility blocker is soundness under free-threading, not API availability:

  • abi3t implies Py_GIL_DISABLED. Enabling it opts every PyO3 class into the free-threaded build’s borrow discipline.

  • The codebase is mid-migration toward free-threading soundness: PyRef<T> → PyClassGuard<T> (task TNXKA2, the two __aiter__ call sites) is done, but the broader audit (GIL release protocol, parking_lot::Mutex under Py_GIL_DISABLED, the Rust-owned bot architecture in docs/architecture/rust-owned-bot.md) is not declared complete.

Adopting abi3t would flip on free-threading expectations before the audit is finished.

3. Does maturin support abi3t wheel tags yet?

Maturin (project pins >=1.0,<2.0, currently resolving 1.13.3) derives the wheel abi tag from the pyo3 InterpreterConfig the Rust build emits — it does not require a separate config knob. Maturin 1.13+ supports the abi3t tag family. This is therefore not a blocker; the blocker is items 1 + 2 (the Python 3.15 floor and the free-threading implication), not maturin.

Decision

Do not replace abi3-py312 with abi3t now. abi3t cannot replace abi3-py312 because:

  1. It requires minimum Python 3.15, but the package declares requires-python = ">=3.12" and CI tests 3.12–3.15. Switching would drop support for every supported Python.

  2. It forces Py_GIL_DISABLED (free-threaded), which the crate has not been fully audited for.

It also cannot meaningfully supplement abi3-py312 in a single wheel: a wheel is either built against the limited stable ABI (abi3, 3.12+) or the truly-stable free-threaded ABI (abi3t, 3.15+) — not both. Shipping a parallel abi3t wheel would only be worth it once there is a real audience on free-threaded Python 3.15+, which there is not yet.

Revisit triggers

Re-open this evaluation when all hold:

  • requires-python floor moves to >=3.15 (or a separate free-threaded wheel is justified by real demand).

  • The free-threading soundness audit is complete (GIL release protocol, Mutex/RwLock discipline under Py_GIL_DISABLED, per the former rust/AGENTS.md, dropped in affebc8de).

  • MINIMUM_SUPPORTED_VERSION_ABI3T / maturin tag support has not regressed across a PyO3 bump.

At that point the change is a one-line Cargo feature swap (abi3-py312 → abi3t-py315) plus confirming the wheel tag, with no Rust source changes expected (the limited-API surface is already in use).