Python Limited API on Embedded Linux: Verify What abi3 Actually Makes Compatible

An abi3 filename is a packaging claim, not a runtime audit. To support several CPython minor versions with one extension binary, select a Limited API baseline, inspect the APIs actually used and test the supported interpreter/platform combinations. Renaming a version-specific extension does not make it compatible.

This guide pins CPython 3.12.14 documentation and headers. An executed x86-64 fixture compiled a small extension with a Python 3.10 Limited API floor and imported it on CPython 3.12.14 only. No other minor interpreter or embedded target was tested.

Separate the source API from the binary promise

The versioned C API stability guide distinguishes the Limited API exposed to source code from the Stable ABI implemented by compatible Python runtimes. The latter includes symbols needed to support older Limited API versions as well as the currently exposed subset.

Define Py_LIMITED_API before including Python.h and choose a fixed minimum supported version. The fixture used 0x030A0000 for Python 3.10. Using the current header’s PY_VERSION_HEX as the floor can silently raise the minimum when the build interpreter changes.

The baseline is a compatibility input that belongs in package metadata and the build command. It is not a promise that an extension can run on an older interpreter than that floor, another CPU architecture or an unrelated C-library ABI.

A fixed Limited API source contract leads through binary API inspection to interpreter testing, with the actual 3.12.14 import and excluded-macro control distinguished from pending versions.
Original abi3 evidence map with a compiled host extension and honest multi-version limits.

Audit the entire extension implementation

List Python C API functions, types, macros and structure fields used by handwritten and generated code. Review each against the selected Limited API baseline. Include optional paths and bundled generated wrappers; checking one top-level source file can miss a private API hidden in generated output.

Private underscore-prefixed APIs and PyUnstable interfaces have different stability contracts. Do not infer that a declaration exposed by the headers is automatically part of the promised Stable ABI. The documentation explicitly warns that defining Py_LIMITED_API is not a complete conformance guarantee.

Inspect the final shared object’s undefined Python symbols and exported initializer. This catches some unexpected dependencies, but not every inline assumption or semantic mismatch. Pair binary inspection with the source-level audit instead of treating either as sufficient alone.

Understand the excluded-macro boundary

The tagged stability guide contrasts PyList_GetItem with the implementation-dependent PyList_GET_ITEM macro. The latter is not available under the Limited API. The 3.12.14 list header also shows the separation between public declarations and the non-limited CPython-specific header inclusion.

The host negative control tried to compile a function using PyList_GET_ITEM with the same Limited API floor and treated implicit declarations as errors. Compilation failed as intended. That demonstrates one useful guard; it does not prove that every private or semantically unsupported operation will be rejected automatically.

When replacing a fast macro with a supported API, review error and reference-ownership semantics. A syntactic replacement can have different preconditions or failure handling. Measure any performance effect in the real workload rather than assuming that API stability is either free or prohibitively expensive.

Read the executed extension result

The fixture module exported an answer function returning a Python integer with value 42. It used PyLong_FromLong, a module definition and PyModule_Create. GCC 14.2.0 compiled it as a shared PIC object with Py_LIMITED_API=0x030A0000 against the available CPython 3.12.14 headers.

The resulting limitedprobe.abi3.so imported successfully in an isolated CPython 3.12.14 child process, and answer returned 42. Dynamic-symbol inspection found PyLong_FromLong and PyModule_Create2 as its undefined Python API symbols.

This is a one-interpreter host result. Building with a 3.10 floor against 3.12 headers does not mean a 3.10 import was performed. The multi-version acceptance matrix remains necessary, especially when an API’s accepted argument semantics evolved after the chosen floor.

Treat abi3 as an assertion to verify

The CPython documentation says that supported platforms may load abi3-tagged extensions without checking their Stable ABI conformance. A loader accepting the filename is therefore not a validation service for the binary’s symbol set or source behavior.

Verify wheel or image metadata separately from the extension suffix. A package must not install a 3.10-minimum extension for an older Python runtime. Keep interpreter constraints, architecture and lower-level shared-library requirements with the artifact identity.

If a dependency exposes its own unstable native ABI, Python’s Stable ABI does not stabilize it. Inspect that dependency’s SONAME, required symbols and supported image combinations independently. The Python-facing boundary is only one layer of the native extension.

Run the supported interpreter matrix

Qualification case Evidence to retain
Oldest supported minor Import, required API behavior and error paths using the exact extension binary.
Newest supported minor Same operations, with warnings and behavior changes reviewed.
Intermediate supported minors Explicit pass or documented untested status, rather than inferred coverage.
Different platform/build variant Separate compatibility decision for compiler, libc, architecture and Python build.

Use the same extension bytes across the compatible-minor tests. Recompiling for every interpreter can accidentally demonstrate source portability while hiding a failure of the one-binary claim. Prefer building against the oldest intended baseline where practical, as the stability guide recommends.

Close with a bounded compatibility statement

Preserve the Limited API floor, compile command, source/API audit, symbol inventory and interpreter results. Include object ownership, exception conversion and teardown behavior in the functional tests, not just module import.

A release statement should name the supported CPython/platform combinations and the exact tested binary. Here only the 3.12.14 host import and one excluded-macro compile control passed. The result is useful evidence toward an abi3 contract, without pretending that the filename or a successful build completes that contract.

Similar Posts