The stable ABI symbol set, abi3 wheel tagging, and the constraints imposed by maintaining ABI compatibility.
The stable ABI is CPython’s binary compatibility interface for extension modules. It lets an extension module be compiled once and loaded by multiple CPython versions, as long as those versions support the requested ABI level.
The normal CPython C API exposes many implementation details. An extension compiled against those details often needs a separate binary wheel for each Python version. The stable ABI reduces that burden by restricting the extension to a smaller, more stable set of functions, types, and macros.
The stable ABI is closely related to the limited API:
| Term | Meaning |
|---|---|
| Limited API | Source-level subset selected at compile time |
| Stable ABI | Binary-level interface promised across CPython versions |
Py_LIMITED_API | Compile-time macro that asks headers to expose the limited API |
abi3 | Wheel tag used for stable-ABI extension binaries |
67.1 Why the Stable ABI Exists
Python extension modules are native binaries. A binary compiled for one CPython version may depend on details that differ in another version.
Examples of unstable details:
object struct layout
type object internals
frame internals
thread state internals
private macros
bytecode details
interpreter state structuresWithout a stable ABI, package authors often publish wheels like:
cp310-cp310-manylinux_x86_64.whl
cp311-cp311-manylinux_x86_64.whl
cp312-cp312-manylinux_x86_64.whl
cp313-cp313-manylinux_x86_64.whlWith the stable ABI, one binary can target multiple CPython versions:
cp38-abi3-manylinux_x86_64.whlThis reduces build matrix size and simplifies distribution.
67.2 Normal ABI vs Stable ABI
The normal CPython ABI gives extensions broad access to CPython implementation details.
Stable ABI restricts that access.
| Area | Normal ABI | Stable ABI |
|---|---|---|
| Object layout | Often visible | Mostly opaque |
| Struct fields | Often directly accessed | Usually hidden |
| Macros | May inspect internals | Restricted or function-backed |
| Compatibility | Version-specific | Cross-version |
| Performance | Maximum possible | Slightly more indirect |
| Power | Full CPython-specific access | Smaller supported surface |
The normal ABI is useful for extensions that need maximum speed or deep CPython integration.
The stable ABI is useful for extensions that value binary portability.
67.3 The Limited API
The limited API is the source-level mechanism for building against the stable ABI.
An extension opts in with:
#define Py_LIMITED_API 0x03080000
#include <Python.h>This means:
use the limited API available since Python 3.8The macro value is a hexadecimal version number:
| Macro value | Target minimum |
|---|---|
0x03080000 | Python 3.8 |
0x03090000 | Python 3.9 |
0x030A0000 | Python 3.10 |
0x030B0000 | Python 3.11 |
0x030C0000 | Python 3.12 |
If you target an older limited API version, your binary can run on more Python versions, but you can use fewer APIs.
67.4 The abi3 Wheel Tag
Python wheels use compatibility tags.
A normal CPython extension wheel may use a tag like:
cp312-cp312-manylinux_x86_64This means it targets CPython 3.12 specifically.
A stable ABI wheel may use:
cp38-abi3-manylinux_x86_64This means:
built for CPython
uses stable ABI
requires at least Python 3.8
platform is manylinux x86_64The abi3 tag is the packaging signal that the extension avoids version-specific CPython ABI dependencies.
67.5 What Becomes Opaque
The stable ABI makes many structures opaque.
Under the full API, code may access fields directly:
Py_TYPE(obj)
Py_REFCNT(obj)
type->tp_name
type->tp_basicsizeUnder the limited API, direct field access is reduced. Extension code should prefer accessor functions and supported APIs.
For example, instead of depending on layout details, use operations such as:
PyObject_Type(obj)
PyObject_GetAttrString(obj, "name")
PyLong_AsLong(obj)
PyUnicode_AsUTF8String(obj)The stable ABI works by preserving callable entry points rather than preserving every internal structure layout.
67.6 Why Struct Layout Is a Problem
C struct layout is part of the binary interface.
Suppose an extension compiles against:
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
int field_a;
} SomeObject;If a later CPython version changes the layout:
typedef struct {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
void *new_field;
int field_a;
} SomeObject;old native code may read the wrong memory offset.
This is why stable ABI code avoids direct layout assumptions. Opaque pointers allow CPython to change internals while keeping function signatures stable.
67.7 Example Stable ABI Extension
A small extension can target the limited API:
#define Py_LIMITED_API 0x03080000
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *
demo_add(PyObject *self, PyObject *args)
{
long a;
long b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
return PyLong_FromLong(a + b);
}
static PyMethodDef methods[] = {
{"add", demo_add, METH_VARARGS, "Add two integers"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT,
"demo",
"Stable ABI demo module",
-1,
methods
};
PyMODINIT_FUNC
PyInit_demo(void)
{
return PyModule_Create(&module);
}This style avoids direct object layout access and uses stable C API functions.
67.8 Building with Setuptools
A minimal setup file:
from setuptools import Extension, setup
setup(
name="demo",
ext_modules=[
Extension(
"demo",
["demo.c"],
py_limited_api=True,
define_macros=[
("Py_LIMITED_API", "0x03080000"),
],
)
],
)For wheels, build configuration must also emit the abi3 tag. Many projects use bdist_wheel options or modern build backend settings for this.
The important rule is that both the compiled extension and the wheel metadata must agree that the extension uses the stable ABI.
67.9 What the Stable ABI Allows
Stable ABI extensions can still do many useful things:
create modules
define functions
create Python objects
parse arguments
raise exceptions
call Python callables
manipulate lists and dicts through API functions
work with Unicode
work with bytes
use capsules
use buffers through supported APIs
define heap types through supported mechanismsFor many extension modules, this is enough.
A wrapper around a C library often fits well:
Python arguments
↓
convert to C values
↓
call native library
↓
convert result to Python objectThis does not usually require direct access to CPython internals.
67.10 What the Stable ABI Restricts
Stable ABI code should avoid:
direct struct field access
private `_Py*` APIs
internal headers
version-specific frame internals
direct bytecode assumptions
direct type object mutation
macros that expand to internal layout access
interpreter private stateSome performance-oriented extensions depend on these details and cannot use the stable ABI easily.
For example, an extension that heavily inspects frame objects, modifies type internals, or uses private vectorcall details may need the full CPython API.
67.11 Static Types vs Heap Types
Stable ABI design prefers heap types over static PyTypeObject definitions.
A static type often requires direct initialization of a PyTypeObject struct:
static PyTypeObject MyType = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "demo.MyType",
.tp_basicsize = sizeof(MyObject),
.tp_new = My_new,
};This exposes dependence on the layout of PyTypeObject.
Heap types use PyType_Spec and slot definitions:
static PyType_Slot My_slots[] = {
{Py_tp_new, My_new},
{Py_tp_dealloc, My_dealloc},
{0, NULL}
};
static PyType_Spec My_spec = {
.name = "demo.MyType",
.basicsize = sizeof(MyObject),
.itemsize = 0,
.flags = Py_TPFLAGS_DEFAULT,
.slots = My_slots,
};Then create the type at runtime:
PyObject *type = PyType_FromSpec(&My_spec);This avoids exposing the full PyTypeObject layout to the extension binary.
67.12 Stable ABI Type Design
A stable ABI extension type should use:
PyType_Spec
PyType_Slot
heap allocation
module state
accessor functions
documented slot IDsPrefer this shape:
module init
create heap type
add type to module
keep state in module
avoid static mutable Python objectsThis aligns better with modern CPython work on subinterpreters and runtime isolation.
67.13 Reference Counting Still Applies
The stable ABI does not remove manual ownership.
This code still returns a new reference:
return PyLong_FromLong(42);This code still requires cleanup:
PyObject *s = PyUnicode_FromString("hello");
if (s == NULL) {
return NULL;
}
Py_DECREF(s);The stable ABI changes what binary symbols and structures the extension can use. It does not change C API ownership rules.
67.14 Performance Tradeoffs
Stable ABI may cost performance in some cases.
Reasons:
less direct field access
more function calls
fewer private fast paths
less access to version-specific optimizationsFor many extensions, this overhead is small because most time is spent in the native library or algorithm.
For extensions that wrap high-frequency Python object operations, the cost can be more visible.
Good candidates:
compression libraries
database bindings
cryptography wrappers
file format parsers
image codecs
native algorithms with coarse callsPoorer candidates:
extensions doing many tiny object-level operations
profilers using private frame internals
debuggers relying on interpreter structures
JIT or bytecode tools
performance-critical custom containers67.15 ABI Compatibility vs API Compatibility
API compatibility means source code still compiles.
ABI compatibility means an existing binary still loads and works.
These are different.
| Compatibility | Question |
|---|---|
| API | Can I recompile this source? |
| ABI | Can I reuse this compiled binary? |
The stable ABI is about ABI compatibility.
An extension using the full C API may remain source-compatible across Python versions, yet still require recompilation for each version.
67.16 The Cost of Depending on Internals
Private CPython internals can be tempting.
Example motivations:
avoid allocation
read type fields directly
access frame state
skip error checks
use private fast paths
reuse internal helper functionsBut each private dependency increases maintenance cost.
Risks:
compile failure on new Python versions
binary crash after upgrade
subinterpreter incompatibility
debug-build differences
free-threading incompatibility
platform-specific bugsStable ABI code gives up these shortcuts in exchange for stronger binary durability.
67.17 Stable ABI and Free-Threaded CPython
Free-threaded CPython work makes ABI discipline more important.
Code that assumes details such as direct reference count layout, global interpreter state, or GIL-protected global mutation may become fragile.
The stable ABI encourages extensions to use documented operations rather than internal fields. This does not automatically make an extension free-threading-safe, but it reduces dependence on implementation details that are likely to evolve.
Thread safety still requires separate design:
protect native state
avoid unsafe globals
respect Python object ownership
use documented thread APIs
avoid hidden interpreter assumptions67.18 Stable ABI and Subinterpreters
Subinterpreters also favor stable, isolated extension design.
Good stable ABI design avoids:
static mutable Python objects
cached interpreter-specific objects
global borrowed references
direct interpreter state pointers
single global module statePrefer:
per-module state
heap types
capsules with immutable function tables
explicit initialization
documented APIsThe stable ABI and subinterpreter-safe design are not identical, but they point in the same direction: fewer hidden process-global assumptions.
67.19 When to Use the Stable ABI
Use the stable ABI when:
you want fewer wheels
your extension wraps a native library
your API surface is modest
you can avoid private CPython internals
binary portability matters more than maximum micro-optimizationUse the full API when:
you need CPython internals
you depend on private performance paths
you implement low-level runtime tooling
you inspect frames deeply
you need APIs outside the limited subsetA library may also split itself:
stable ABI core wrapper
+
version-specific optional acceleratorThis gives broad compatibility with optional specialized speedups.
67.20 Practical Checklist
Before choosing stable ABI, check:
| Question | Good answer |
|---|---|
Can the extension avoid private _Py* APIs? | Yes |
| Can it avoid direct struct field access? | Yes |
| Can it use heap types instead of static types? | Yes |
| Is most work done in native code rather than Python object churn? | Yes |
| Does it need frame or bytecode internals? | No |
| Does it require one wheel across Python versions? | Yes |
If most answers align, stable ABI is a strong choice.
67.21 Common Mistakes
| Mistake | Result |
|---|---|
Defining Py_LIMITED_API but using private APIs | Build or runtime failure |
| Building with limited API but publishing wrong wheel tag | Installer confusion |
Directly initializing static PyTypeObject | ABI coupling |
| Assuming stable ABI gives source portability to all versions | It only covers selected APIs |
| Ignoring reference ownership | Leaks or crashes |
| Caching interpreter-specific globals | Subinterpreter bugs |
| Using macros that access struct fields | Breaks limited API discipline |
Stable ABI requires consistent build, code, and packaging choices.
67.22 Chapter Summary
The stable ABI is CPython’s cross-version binary interface for extension modules. It lets one compiled extension binary work across multiple CPython versions by restricting code to the limited API.
The limited API is selected with Py_LIMITED_API. The resulting wheel usually uses the abi3 tag. This reduces wheel build matrices and improves binary portability.
The tradeoff is reduced access to CPython internals. Stable ABI extensions should avoid direct struct access, private APIs, static type layout assumptions, and interpreter-specific global state.
For many native wrappers, codecs, parsers, database drivers, and compute libraries, the stable ABI is a practical default. For extensions that need deep runtime internals or maximum object-level speed, the full CPython API remains necessary.