96. Stable ABI Design Tradeoffs
What breaking the stable ABI costs, historical breakages, versioning policy, and the tradeoff against new features.
96. Stable ABI Design Tradeoffs
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:
| Runtime | Challenge |
|---|---|
| PyPy | Different GC and object model |
| GraalPython | JVM runtime |
| HPy-oriented runtimes | Abstract object handles |
| Experimental CPython runtimes | Internal 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:
| Layer | Purpose |
|---|---|
| Full C API | Maximum power and performance |
| Limited API | Safer abstraction |
| Stable ABI | Binary compatibility |
| Internal APIs | CPython 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.