Skip to content

96. Stable ABI Design Tradeoffs

What breaking the stable ABI costs, historical breakages, versioning policy, and the tradeoff against new features.

The CPython Stable ABI is a compatibility layer that allows extension modules compiled against one Python version to continue working across multiple later Python versions without recompilation.

The Stable ABI exists to reduce binary compatibility problems in the Python extension ecosystem.

Without a stable ABI:

extension compiled for Python 3.x
    may fail on Python 3.y

because internal structures, function layouts, macros, or object representations change.

With the Stable ABI:

one compiled extension binary
    works across many CPython versions

This chapter examines:

what ABI stability means
how the Stable ABI differs from the full C API
why ABI stability is difficult
how CPython hides implementation details
tradeoffs between performance and compatibility
limitations of the Stable ABI
interaction with runtime evolution
future compatibility pressures

The Stable ABI is one of the central engineering compromises in CPython.

96.1 API vs ABI

An API and an ABI are related but different.

API

An API defines source-level interfaces.

Example:

PyObject *PyLong_FromLong(long v);

A program including Python headers can compile against this declaration.

ABI

An ABI defines binary-level compatibility.

It includes:

calling conventions
structure layouts
symbol names
memory alignment
register usage
binary object representation

Two versions may share the same API but break ABI compatibility.

Example:

typedef struct {
    PyObject_HEAD
    long value;
} PyLongObject;

If CPython changes this structure layout:

old extension binary may read wrong memory offsets

leading to crashes or corruption.

The Stable ABI attempts to prevent such breakage.

96.2 Why Binary Compatibility Matters

Python has a massive native extension ecosystem.

Examples include:

NumPy
pandas
cryptography
Pillow
lxml
SciPy
database drivers
machine learning libraries

Compiling extensions repeatedly for every Python release is expensive.

Without ABI stability:

new Python release
all extension wheels rebuilt

This creates:

distribution overhead
CI complexity
installation failures
platform fragmentation
slow ecosystem adoption

A stable ABI reduces this burden.

96.3 CPython’s Historical C API

Historically, CPython exposed many internal implementation details directly.

Extensions could access object internals freely:

PyListObject *list;
list->ob_item[i]

or:

Py_TYPE(obj)
obj->ob_refcnt

This made extensions fast and flexible.

But it tightly coupled extensions to CPython internals.

If CPython changed:

object layout
allocator behavior
header structure
reference counting semantics

old binaries could break immediately.

The historical API prioritized performance and direct access over abstraction.

96.4 The Stable ABI Goal

The Stable ABI attempts to separate:

public binary interface

from:

internal runtime implementation

The key idea:

extensions should use stable exported functions
instead of relying on internal structures

This allows CPython internals to evolve while preserving binary compatibility.

96.5 The Limited API

The Stable ABI is closely connected to the Limited API.

The Limited API restricts which symbols and structures extensions may use.

Extensions opting into the Limited API define:

#define Py_LIMITED_API

before including Python headers.

This changes visible declarations.

Instead of exposing internals directly, the headers expose opaque interfaces.

Example:

PyObject *obj;

is still visible.

But internal structure fields may become hidden.

Extensions must then use accessor functions instead of direct structure access.

96.6 Opaque Object Structures

One major design strategy is opaque structures.

Historically:

obj->ob_refcnt
obj->ob_type

were directly accessible.

In a stable ABI model:

internal layout should be hidden

Instead of:

Py_TYPE(obj)

extensions may need:

PyObject_Type(obj)

or another exported API.

Opaque structures allow CPython to modify internal layouts safely.

96.7 Why Direct Structure Access Is Dangerous

Direct structure access hardcodes layout assumptions into compiled binaries.

Suppose CPython changes:

typedef struct {
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

to:

typedef struct {
    uintptr_t flags;
    Py_ssize_t ob_refcnt;
    PyTypeObject *ob_type;
} PyObject;

An old extension expecting the original layout may:

read wrong memory
write invalid offsets
corrupt runtime state
crash

Opaque APIs prevent extensions from depending on exact memory layouts.

96.8 Stable ABI Symbol Export

The Stable ABI relies on exported runtime symbols.

Extensions compiled against the Stable ABI call functions dynamically resolved at runtime.

Examples:

PyLong_FromLong
PyList_New
PyObject_GetAttrString
PyImport_ImportModule

These exported functions remain ABI-stable across versions.

Internals behind them may change freely.

96.9 Why Macros Are Problematic

Traditional CPython uses many macros for speed.

Example:

#define PyList_GET_ITEM(op, i) (((PyListObject *)(op))->ob_item[i])

Macros inline structure access directly into extension binaries.

This is fast, but destroys ABI flexibility.

If list layout changes:

compiled macro expansion becomes invalid

Stable ABI code therefore prefers functions:

PyObject *PyList_GetItem(PyObject *list, Py_ssize_t index);

Function calls preserve abstraction boundaries.

96.10 Performance Tradeoffs

This creates one of the core tradeoffs.

Direct Access

Advantages:

faster
inlinable
minimal overhead
full implementation visibility

Disadvantages:

ABI fragile
implementation tightly coupled
runtime evolution constrained

Opaque API

Advantages:

binary compatibility
implementation flexibility
safer runtime evolution

Disadvantages:

extra call overhead
less optimization opportunity
reduced low-level control

The Stable ABI intentionally sacrifices some performance flexibility for compatibility.

96.11 Runtime Evolution Pressure

Modern CPython increasingly changes runtime internals:

free-threaded execution
immortal objects
specialized interpreters
new allocator strategies
inline caches
layout optimizations

These changes become difficult if extensions depend on internal layouts.

Stable ABI abstraction helps CPython evolve internally without breaking extensions.

This is increasingly important as interpreter architecture becomes more sophisticated.

96.12 Reference Counting and ABI Stability

Reference counting creates compatibility challenges.

Historically:

Py_INCREF(obj);
Py_DECREF(obj);

were often macros manipulating object fields directly.

Future runtimes may change refcount behavior:

atomic refcounts
biased refcounts
immortal objects
thread-local ownership

If extensions directly manipulate object headers, runtime evolution becomes constrained.

Stable APIs help decouple extension code from internal memory management strategies.

96.13 Stable ABI and Free-Threading

Free-threaded CPython increases pressure for abstraction.

Traditional extensions often assume:

direct refcount access
borrowed reference safety under GIL
unlocked structure access
global runtime assumptions

These assumptions become unsafe under concurrency.

A more opaque ABI boundary allows CPython to change synchronization mechanisms internally.

Without abstraction, many runtime improvements become nearly impossible.

96.14 Heap Types vs Static Types

Static type definitions historically embedded interpreter assumptions directly into binaries.

Example:

static PyTypeObject MyType = {
    PyVarObject_HEAD_INIT(NULL, 0)
};

Heap types are more flexible.

They are created dynamically at runtime:

runtime constructs type object
runtime controls layout
runtime may evolve implementation

Heap types better support stable interfaces because they reduce static binary coupling.

96.15 The Cost of Abstraction

Every abstraction layer introduces cost.

Example:

PyList_GET_ITEM(list, i)

may compile into one memory load.

But:

PyList_GetItem(list, i)

requires:

function call
argument passing
possible checks
less compiler optimization

These differences matter in tight loops.

High-performance extensions sometimes avoid the Stable ABI for this reason.

96.16 Why Some Extensions Avoid the Stable ABI

Many performance-sensitive extensions still target the full CPython API instead of the Stable ABI.

Reasons include:

maximum performance
direct memory access
macro-based fast paths
custom allocator integration
internal structure knowledge
advanced runtime manipulation

Examples:

scientific computing
vectorized numeric libraries
custom runtime integrations
advanced debuggers
profilers

The Stable ABI is useful, but not universally optimal.

96.17 Stable ABI Versioning

The Stable ABI evolves conservatively.

CPython guarantees that certain exported interfaces remain available across versions.

Extensions compiled for older Stable ABI targets can often run on newer interpreters unchanged.

Conceptually:

extension compiled against stable ABI version X
runs on future CPython versions

This reduces wheel explosion across Python versions.

96.18 ABI Stability Limits

The Stable ABI cannot guarantee everything.

Certain behaviors still depend on runtime semantics:

threading behavior
GC timing
performance characteristics
object identity details
allocator behavior
internal optimization strategies

The Stable ABI preserves binary compatibility, not necessarily identical runtime behavior.

96.19 Stable ABI and Inlining

Inlining is difficult across opaque ABI boundaries.

Direct structure access allows compilers to optimize aggressively.

Opaque calls reduce visibility.

Example:

compiler cannot easily inline opaque runtime behavior

This limits:

constant propagation
dead code elimination
register allocation optimization
specialized memory access

The Stable ABI therefore constrains certain compiler optimizations.

96.20 Compatibility vs Innovation

This is the central tension.

CPython wants:

runtime innovation
better scalability
new memory models
better concurrency
layout optimization

But the ecosystem wants:

stable binaries
minimal rebuilds
predictable compatibility

The Stable ABI balances these competing goals.

Too much exposure freezes internals permanently.

Too much abstraction hurts performance and flexibility for extension authors.

96.21 Borrowed References and ABI Safety

Borrowed references are another pressure point.

Traditional APIs expose patterns like:

PyObject *item = PyList_GET_ITEM(list, i);

This depends heavily on runtime invariants.

Future runtimes with:

free-threading
moving GC
different ownership models

may require safer APIs.

The Stable ABI increasingly encourages explicit ownership semantics.

96.22 Stable ABI and Alternate Runtimes

A cleaner ABI boundary also helps alternate Python runtimes.

If extensions depend less on CPython internals:

other runtimes can emulate the ABI more easily

This improves portability across:

RuntimeChallenge
PyPyDifferent GC and object model
GraalPythonJVM runtime
HPy-oriented runtimesAbstract object handles
Experimental CPython runtimesInternal layout changes

Opaque interfaces improve runtime flexibility.

96.23 HPy and Future Directions

HPy is a newer extension API effort designed around stronger abstraction.

Instead of exposing raw PyObject * everywhere:

opaque handles
explicit lifetime management
runtime-neutral API boundaries

HPy aims to support:

CPython
PyPy
future runtimes
free-threaded interpreters
moving garbage collectors

more cleanly than the traditional API.

It represents a possible future direction for extension compatibility.

96.24 Why CPython Cannot Fully Hide Internals

Completely hiding internals is difficult.

Some extensions fundamentally require low-level access:

NumPy array internals
custom allocators
JIT integrations
debugging tools
profilers
runtime instrumentation

CPython therefore maintains multiple layers:

LayerPurpose
Full C APIMaximum power and performance
Limited APISafer abstraction
Stable ABIBinary compatibility
Internal APIsCPython implementation use

This layered approach reflects ecosystem diversity.

96.25 Mental Model

Use this model:

The full CPython API exposes implementation details directly.

This gives:
    high performance
    deep runtime access
    tight coupling to internals

The Stable ABI hides implementation details behind stable exported interfaces.

This gives:
    binary compatibility
    safer runtime evolution
    lower direct optimization freedom

The Stable ABI is therefore an engineering compromise between performance and long-term compatibility.

96.26 Chapter Summary

The CPython Stable ABI allows extension modules to remain binary-compatible across multiple Python versions.

Achieving ABI stability requires hiding implementation details behind stable interfaces:

opaque structures
stable exported functions
restricted APIs
reduced direct memory access

This abstraction enables runtime evolution, including:

free-threading
new refcount strategies
layout optimization
allocator redesign

But it also introduces tradeoffs:

less optimization freedom
higher abstraction overhead
reduced low-level control

The Stable ABI is fundamentally a balance between compatibility, performance, and runtime evolution flexibility.