Following system colour scheme Selected dark colour scheme Selected light colour scheme

Python Enhancement Proposals

PEP 803 – “abi3t”: Stable ABI for Free-Threaded Builds

PEP 803 – “abi3t”: Stable ABI for Free-Threaded Builds

Author:
Petr Viktorin <encukou at gmail.com>, Nathan Goldbaum <nathan.goldbaum at gmail.com>
Discussions-To:
Discourse thread
Status:
Draft
Type:
Standards Track
Requires:
703, 793, 697
Created:
19-Aug-2025
Python-Version:
3.15
Post-History:
08-Sep-2025, 20-Nov-2025, 16-Feb-2026

Table of Contents

Abstract

Add a new variant of the Stable ABI, called “Stable ABI for Free-Threaded Python” (or abi3t for short), and a corresponding API limitations.

abi3t will be based on the existing Stable ABI (abi3), but make the PyObject structure opaque. This will require users to migrate to new API for common tasks like defining modules and most classes.

At least initially, extensions built for abi3t will be compatible with the existing Stable ABI (abi3).

Terminology

This PEP uses “GIL-enabled build” as an antonym to “free-threaded build”, that is, an interpreter or extension built without Py_GIL_DISABLED.

Motivation

The Stable ABI is currently not available for free-threaded builds. Extensions will fail to build for a both Limited API and free-threaded Python (that is, when both Py_LIMITED_API and Py_GIL_DISABLED preprocessor macros are defined). Extensions built for GIL-enabled builds of CPython will fail to load (or crash) on free-threaded builds.

In its acceptance post for PEP 779, the Steering Council stated that it “expects that Stable ABI for free-threading should be prepared and defined for Python 3.15”.

This PEP proposes the Stable ABI for free-threading.

Background

Python’s Stable ABI (abi3 for short), as defined in PEP 384 and PEP 652, provides a way to compile extension modules that can be loaded on multiple minor versions of the CPython interpreter. Several projects use this to limit the number of wheels (binary artefacts) that need to be built and distributed for each release, and/or to make it easier to test with pre-release versions of Python.

With free-threading builds (PEP 703) being on track to eventually become the default (PEP 779), we need a way to make a Stable ABI available to those builds.

To build against a Stable ABI, the extension must use a Limited API, that is, only a subset of the functions, structures, etc. that CPython exposes. Both the Limited API and the current Stable ABI are versioned, and building against Stable ABI 3.X requires using only Limited API 3.X, and yields an extension that is ABI-compatible with CPython 3.X and any later version (though bugs in CPython sometimes cause incompatibilities in practice).

The Limited API is not “stable”: newer versions may remove API that were a part of older versions.

This PEP proposes additional API limitations, as required to be compatible with both GIL-enabled and free-threaded builds of CPython.

Ecosystem maintainers want decreased maintenance burden

A major advantage of the limited API and stable ABI wheels is that new Python versions are supported on the day of release. Without stable ABI wheels, maintainers are left with a choice between closely following CPython releases and producing wheels during CPython beta periods or dealing with inevitable user requests for support for new CPython versions.

Cryptography

The cryptography project shipped 48 wheel files with their most recent release. Cryptography is somewhat unusual in that they ship 14 wheels each for both the cp38 and cp311 stable ABIs to enable optimizations available in newer limited API versions. They also ship 14 additional cp314t wheels and 6 wheels for pypy. If there is no free-threaded stable ABI, then with Python 3.15, cryptography will be using roughly the same amount of space on PyPI to support two versions of the free-threaded build as all non-EOL versions of the GIL-enabled build.

Cryptography maintainer Alex Gaynor expressed a desire on Discourse for a free-threaded stable ABI:

Just to state this explicitly from the PyCA maintainers perspective, as long as we have O(1) builds, that’s ok. What we can’t/won’t do is O(n) where we need new builds for every Python release.

When one of the PEP authors asked Alex in the #pyca Libera IRC channel for his current opinion, he said:

One other thing I’ll note that’s really valuable about abi3 is that it means our old wheels keep working for new Python versions. If we have per-Python release wheels, we have to do a bunch of work at various points in the python release cycle (including potentially backport releases to add new wheels, if we’re not otherwise planning a release at that time).

As maintainers, we really like to structure our work to avoid being “on the clock” like that.

moocore

The moocore project ships seven abi3 wheels. When the topic of adding support for the free-threaded build came up on the moocore issue tracker, maintainer Manuel López-Ibáñez let the person reporting the issue know that:

I don’t want to build for 3.14 free-threading unless you really need it.

Later, after discovering the tracking issue for supporting the limited API on the free-threaded build, he commented:

By the way, python/cpython#111506 is about extending the stable ABI to support free-threaded Python. If they do that, then the builds of moocore will work in both classical and free-threaded Python versions, without needing to build new wheels for each Python version.

[…]

I will revisit this again once Python 3.15 is released. Hopefully the ABI will be stable (or even better, free-threading will be the default).

Pydantic

Pydantic maintainer David Hewitt observed:

Pydantic distributes wheels for a native core built using Rust & PyO3. The latest release of pydantic-core distributed 112 wheels and this number is set to grow as more environments are to be added (e.g. Android, iOS, wasm). Pydantic has historically not distributed using the stable ABI because the feature set was too immature. Much Pydantic functionality interacts with Python objects via the C API in hot loops so performance is key concern. As the stable ABI matures it will be ideal for Pydantic to switch tier 2 platforms to the stable ABI (and perhaps eventually tier 1 platforms too), which will significantly reduce the number of wheels to build, test, and distribute.

I would like to highlight that if free-threading does not adopt a stable ABI, all the benefits above will be lost when free-threading becomes the default and only build (which seems the expected long-term plan).

SciPy

The SciPy project uploaded 60 version-specific wheel files for its last release to support four different CPython versions. They do not upload wheels for PyPy.

When asked about this proposal, SciPy steering council chair Ralf Gommers said:

SciPy and a number of other projects in the scientific Python ecosystem are quite interested in starting to use the Stable ABI, in particular to reduce the maintenance load of providing more wheels. With recent CPython, Cython and NumPy releases, this now seems possible. The performance costs seem acceptable and small, although we’ll only really build confidence in that assessment after having made the switch.

By providing a new free-threaded stable ABI in Python 3.15, SciPy will not have to consider the lack of a stable ABI on the free-threaded build as the project considers switching to stable ABI wheels.

Bindings generators

Both moocore and cryptography use bindings generators to interface with the C API. Cryptography uses PyO3 and CFFI while moocore uses only CFFI. Both CFFI and PyO3 already handle all the details of abstracting over the C API to enable different build configurations and there is no need to laboriously port extension types to use APIs that are only available on one build or another.

Using bindings generators will enable these projects to quickly adopt the new stable ABI. Initial testing using the experimental _Py_OPAQUE_PYOBJECT flag defined in CPython’s main branch, indicates that PyO3, CFFI, and Cython will all work with PEP 803 using packaging tools that have been patched to account for an abi3.abi3t tag.

PyO3 maintainer David Hewitt said in support of this proposal:

PyO3 greatly benefits from having a stable ABI - one of the biggest challenges for the framework is the need to abstract over a wide range of Python / OS / CPU / environment combinations. We also offer the possibility to build with the stable ABI for each of these environments (targeting a given minimum Python version’s stable ABI). The goal is always that all functionality PyO3 offers works the same on all these combinations (sometimes with a Python version floor to access certain features). We currently support Python 3.7+. All functionality added to the stable ABI is a very welcome promise that PyO3 will not need to introduce further conditional code to support a given feature. In the long run this makes it possible for PyO3 to simplify code paths as support for older Python versions is dropped, helping to keep maintenance burden under control.

When asked to comment about this proposal, Cython maintainer David Woods said:

Cython doesn’t have huge problems with the number of wheels we distribute because ultimately it works fine as pure-Python. We do distribute wheels for a few of the smaller platforms as Stable ABI wheels but that’s more “dogfooding” than because we actually need to. So I’m adding this in anticipation that other people will find it useful rather than because I will.

I do remain slightly concerned that the performance trade-offs for this will turn out to be too much for many Cython users (it’s possible that the trade-off may be different for other binding tools). That’s not a huge disaster since we’re not getting rid of the regular compilation mode so people are free to pick their own personal trade-offs.

Rationale

The design in this PEP uses several constraints:

Separate ABI
The new ABI (abi3t) will conceptually be treated as separate from the existing Stable ABI (abi3).

However, it will be possible to compile a single extension module that supports both free-threaded and GIL-enabled builds. This should involve no additional limitations, at least in the initial implementation, and so it will be preferred over building abi3t-only extensions.

No backwards compatibility now
The new stable ABI variant will not support CPython 3.14 and below. Projects that need this support can build separate extensions specifically for the 3.14 free-threaded interpreter, and for older stable ABI versions.

However, we won’t block the possibility of extending compatibility to CPython 3.14 and below, and we recommend that package installation tools prepare for such extensions. See a rejected idea for how this could work.

API changes are OK
The new Limited API variant may require extension authors to make significant changes to their code. Projects that cannot do this (yet) can continue using the existing Limited API, and compile separately for GIL-enabled builds and for specific versions of free-threaded builds.

Tag name

The tag abi3t is chosen to reflect the fact that this ABI is similar to abi3, with minimal changes necessary to support free-threading (which uses the letter t in existing, version-specific ABI tags like cp314t).

Specification

Stable ABI for free-threaded builds

Python will introduce a new stable ABI, called stable ABI for free-threading builds, or abi3t for short. As with the current Stable ABI (abi3), abi3t will be versioned using major (3) and minor versions of Python interpreter. Extensions built for abi3t 3.x will be compatible with CPython 3.x and above.

To build a C/C++ extension for abi3t, the extension will need to only use limited API for free-threaded builds. Like the existing Limited API, this will be a subset of the general CPython C API. Initially, it will also be a subset of the Limited API. We will strive to keep it as a subset, but will not guarantee this.

Since abi3t and abi3 will overlap, it will be possible for a single compiled extension to support both at once, and thus be compatible with CPython 3.15+ (both free-threaded and GIL-eanbled builds). Initially, any extension compiled for abi3t will be compatible with abi3 as well.

Choosing the target ABI

Users of the C API – or build tools acting on their behalf, configured by tool-specific UI – will select the target ABI using the following macros:

  • Py_LIMITED_API=<version> (existing): Compile for abi3 of the given version.
  • Py_TARGET_ABI3T=<version> (proposed here): Compile for abi3t of the given version.

These two macros are functionally very similar. In hindsight, Py_TARGET_ABI3 (without the T) would be a more fitting name for Py_LIMITED_API. We keep the existing name for backwards compatibility.

For ease of use and implementation simplicity, respectively, Python.h will set the configuration macros automatically in the following situations:

  • If Py_LIMITED_API=v and Py_GIL_DISABLED is set, then Py_TARGET_ABI3T will be defined as v by default. (This allows choosing abi3t by defining the pre-existing macro and compiling with free-threaded CPython headers. Note that in CPython 3.14, this case results in a compile-time error.)
  • If Py_TARGET_ABI3T=v is set, CPython may define or redefine Py_LIMITED_API as v. (This means that CPython can continue to use the Py_LIMITED_API macro internally to select which APIs are available.)

Opaque PyObject

abi3t will initially have a single diffenerce from abi3: the PyObject structure and APIs that depend on it are not part of abi3t.

Specifically, when building for abi3t, the CPython headers will:

In both the regular stable ABI (abit3 3.15+) and the new abi3t, the following will be exported functions (exposed in the ABI) rather than macros:

Implications

Making the PyObject, PyVarObject and PyModuleDef structures opaque means:

  • Their fields may not be directly accessed.

    For example, instead of o->ob_type, extensions must use Py_TYPE(o). This usage has been the preferred practice for some time.

  • Their size and alignment will not be available. Expressions such as sizeof(PyObject) will no longer work.
  • They cannot be embedded in other structures. This mainly affects instance structs of extension-defined types, which will need to be defined using API added in PEP 697 – that is, using a struct without PyObject (or other base class struct) at the beginning, with PyObject_GetTypeData() calls needed to access the memory.
  • Variables of these types cannot be created. This mainly affects static PyModuleDef variables needed to define extension modules. Virtually all extensions will need to switch the new export hook added in PEP 793 (PyModExport_modulename()) to support abi3t.

The following functions will become practically unusable in abi3t, since an extension cannot create valid, statically allocated, input for them. They will, however, not be removed.

Runtime ABI checks

Users – or rather build/install tools acting on users’ behalf – will continue to be responsible for not putting incompatible extensions on Python’s import paths. This decision makes sense since tools typically have much richer metadata than what CPython can check. Typically, build tools and installers use PyPA packaging metadata and platform compatibility tags to communicate compatibility details, but other models are possible.

However, CPython will add a line of defense against outdated or misconfigured tools, or human mistakes, in the form of a new module slot containing basic ABI information. This information will be checked when a module is loaded, and incompatible extensions will be rejected. The specifics are left to the C API working group (see issue 72).

This slot will become mandatory with the new export hook added in PEP 793. (That PEP currently says “there are no required slots”; it will be updated.)

Check for non-free-threaded ABI in free-threading builds

Additionally, in free-threaded builds, PyModuleDef_Init() will detect extensions using the non-free-threading Stable ABI, emit an informative message when one is loaded, and raise an exception. (Implementation note: A message will be printed before raising the exception, because extensions that attempt to handle an exception using incompatible ABI will likely crash and lose the exception’s message.)

This check for non-free-threading abi3 relies on internal bit patterns and may be removed in future CPython versions, if the internal object layout needs to change.

The abi3t wheel tag

Wheels that use a stable ABI compatible with free-threading CPython builds should use a new ABI tag: abi3t.

Recommendations for installers

Package installers should treat this tag as completely separate from abi3. They should allow abi3t-tagged wheels for free-threaded builds wherever they currently allow abi3-tagged ones for (orherwise equal) non-free-threaded builds.

Recommendations for build tools

Build tools should give users one or two additional options, in addition to the existing CPython version-specific ABI (cp3nn) and Stable ABI (abi3):

  • Compile extensions compatible with both abi3 and abi3t, by either:
    • defining both Py_LIMITED_API=v and Py_TARGET_ABI3T=v, or
    • defining Py_LIMITED_API=v and:
      • defining Py_GIL_DISABLED (on Windows)
      • building with free-threaded CPython headers (elsewhere)

    Such extensions should be tagged with the compressed tag set abi3.abi3t.

  • Compile extensions compatible with only abi3t, by defining only Py_TARGET_ABI3T=v and tagging the result with abi3t. This will initially offer no advantages over the abi3.abi3t option above, but there is a possibility that it will become useful in the future.

In the above, v stands for a the lowest Python version with which the extension should be compatible, in Py_PACK_VERSION() format. In the cases above, this version must be set to 3.15 or higher.

The version of the Stable ABI, both abi3 and abi3t, is indicated by the Python wheel tag. For example, a wheel tagged cp315-abi3.abi3t will be compatible with 3.15, 3.16, and later versions; cp317-abi3.abi3t will be compatible with 3.17+.

Note that this PEP does not provide a way to target Stable ABI for Free-threaded Python 3.14 and below (cp314-abi3t). This may change an the future, or with experimental build tools, so installers should be prepared for such extensions.

New API

Implementing this PEP will make it possible to build extensions that can be successfully loaded on free-threaded Python, but not necessarily ones that are thread-safe without a GIL.

Limited API to allow thread-safety without a GIL – presumably PyMutex, PyCriticalSection, and similar – will be added via the C API working group, or in a follow-up PEP.

Backwards and Forwards Compatibility

Extensions targetting abi3t will not be backwards-compatible with older CPython releases, due to the need to use new PyModExport API added in PEP 793.

Extension authors who cannot switch may continue to use the existing abi3, that is, build on GIL-enabled Python without defining Py_GIL_DISABLED. For compatibility with free-threaded builds, they can compile using version-specific ABI – that is, compile abi3-compatible source on free-threaded CPython builds without defining Py_LIMITED_API.

Limited API 3.15 for free-threading is a subset of the existing Limited API, and as such, it will be forward-compatible with future versions of CPython 3.x. Older versions of the Limited API (that is, 3.14 and below) will continue to be forward-compatible with GIL-enabled builds of CPython 3.x, starting with the version that introduced the given Limited API.

Compatibility Overview

The following table summarizes compatibility of wheel tags with CPython interpreters. “GIL” stands for GIL-enabled interpreter; “FT” stands for a free-threaded one.

Wheel tag 3.14 (GIL) 3.14 (FT) 3.15 (GIL) 3.15 (FT) 3.16+ (GIL) 3.16+ (FT)
cp314-cp314
cp314-cp314t
cp314-abi3
cp314-abi3t (*)
cp314-abi3.abi3t (*)
cp315-cp315
cp315-cp315t
cp315-abi3
cp315-abi3t
cp315-abi3.abi3t

(*): Wheels with these tags cannot be built; see table below

The following table summarizes which wheel tag should be used for an extension built with a given interpreter and Py_LIMITED_API macro:

To get the wheel tag… Compile on… Py_LIMITED_API Py_TARGET_ABI3T Note
cp314-cp314 3.14 (GIL) N/A existing
cp314-cp314t 3.14 (FT) N/A existing
cp314-abi3 3.14+ (GIL) 3.14 N/A existing
cp314-abi3t N/A reserved
cp314-abi3.abi3t N/A reserved
cp315-cp315 3.15 (GIL) continued
cp315-cp315t 3.15 (FT) continued
cp315-abi3 3.15+ (GIL) 3.15 continued
cp315-abi3t 3.15+ 3.15 new
cp315-abi3.abi3t 3.15+ (FT) 3.15 new
3.15+ 3.15 3.15

In the “Compile on” column, FT means that the Py_GIL_DISABLED macro must be defined – either explicitly or, on non-Windows platforms, by including CPython headers configured with --disable-gil. GIL means that Py_GIL_DISABLED must not be defined.

In the Py_LIMITED_API and Py_TARGET_ABI3T, a dash means the macro must not be defined; a version means the macro must be set to the corresponding integer in Py_PACK_VERSION() format.

Values in the Note column:

  • existing: The wheel tag is currently in use
  • continued: The wheel tag continues the existing scheme
  • new: Proposed in this PEP.
  • reserved: A mechanism to build a matching extension is not proposed in this PEP, but may be added in the future. Installers should be prepared to handle the tag.

Security Implications

None known.

How to Teach This

A porting guide will need to explain how to move to APIs added in PEP 697 (Limited C API for Extending Opaque Types) and PEP 793 (PyModExport).

Reference Implementation

This PEP combines several pieces, implemented individually:

  • Opaque PyObject is available in CPython main branch after defining the _Py_OPAQUE_PYOBJECT macro. Implemented in GitHub pull request python/cpython#136505.
  • For PyModExport, see PEP 793 and GitHub issue #140550.
  • A version-checking slot was implemented in GitHub pull request python/cpython#137212.
  • A check for older abi3 was implemented in GitHub pull request python/cpython#137957.
  • For wheel tags, there is no implementation yet.
  • A porting guide is not yet written.

Rejected Ideas

Make PyObject opaque in Limited API 3.15

It would be possible to make PyObject struct opaque in Limited API 3.15 (that is, the new version of the existing Limited API), rather than introduce a new variant of the Stable ABI and Limited API.

This would mean that extension authors would need to adapt their code to the new limitations, or abandon Limited API altogether, in order to use any C API introduced in Python 3.15.

It would also not remove the need for a new wheel tag (abi3t), which would be required to express that an extension is compatible with both GIL-enabled and free-threaded builds of CPython 3.14 or lower.

Shims for compatibility with CPython 3.14

It’s possible to build a cp314-abi3.abi3t extension – one compatible with 3.14 (both free-threaded build and default). There are several challenges around this:

  • making it convenient and safe for general extensions
  • testing it (as CPython’s test suite doesn’t involve other CPython versions than the one being tested)

So, providing a mechanism to build such extensions is best suited to an external project (for example, one like pythoncapi-compat). It’s out of scope for CPython’s C API, and this PEP.

To sketch how such a mechanism could work:

The main issue that prevents compatibility with Python 3.14 is that with opaque PyObject and PyModuleDef, it is not feasible to initialize an extension module. The solution, PEP 793, is only being added in Python 3.15.

It is possible to work around this using the fact that the 3.14 ABIs (both free-threading and GIL-enabled) are “frozen”, so it is possible for an extension to query the running interpreter, and for 3.14, use a struct definition corresponding to the detected build’s PyModuleDef.

Naming this abi4

Instead of abi3t, we could “bump the version” and use abi4 instead. The difference is largely cosmetic.

However, one thing this PEP does not propose is changing the filename tag: extensions will be named with the extensions like .abi3.so. Changing this while keeping compatibility with GIL-enabled builds would be an unnecessary technical change.

Using abi3.abi4 in wheel tags but only .abi3 in filenames would look more inconsistent than abi3.abi3t and .abi3.

If we added an abi4 tag, the Py_LIMITED_API value would either need to:

  • change to start with 4 to match abi4, but no longer correspond to PY_VERSION_HEX (making it harder to generate and check), or
  • not change, making it inconsistent with abi4.

Adding abi3t is a smaller change than adding abi4, making it work better as a transitional state before larger changes like PEP 809’s abi2026.

Reusing Py_GIL_DISABLED to enable the new ABI

It would be possible to select abi3t (rather than abi3) when the Py_GIL_DISABLED macro is defined together with Py_LIMITED_API.

This would require annoying fiddling with build flags, and make it especially cumbersome to target both abi3 and abi3t at the same time.

Making the new version of abi3 compatible with free-threading

It would be possible to make PyObject opaque in Limited API 3.15, rather than add a new stable ABI. This would make all extensions built for the Stable ABI 3.15 and above compatible with both free-threading and GIL-enabled Python.

In the PEP discussion, the ability to build for the GIL-only Stable ABI with no source changes was deemed to be worth an extra configuration macro (Py_TARGET_ABI3T).


Source: https://github.com/python/peps/blob/main/peps/pep-0803.rst

Last modified: 2026-02-27 16:55:35 GMT