Skip to content
AITroveRead. Build. Understand.
Make this comfortable

Python Limited API: build an abi3 extension without claiming a full release matrix

Last updated: 1 Oct 20265 min read
tutorial
IntermediateBy AITrove Editorial

The Limited API restricts C symbols; an abi3-named build still needs tests on every supported runtime and platform.

Download Python source kit

Operation contract

The fixture compiles a fee calculator with Py_LIMITED_API set to the Python 3.11 API level and writes an abi3-suffixed shared object. It imports that binary on the checked interpreter, adds seven units to forty-seven, and rejects a negative amount. The C code uses only a small stable-interface surface and checks arithmetic before addition.

Failure boundary

A successful import on CPython 3.14 does not verify CPython 3.11, a free-threaded build, Linux, or Windows. The abi3 filename alone does not certify that every symbol used is in the stable ABI or that the wheel tag is correct. Packaging needs a real build backend, supported-platform wheels, and cross-version import tests. This fixture builds no distributable wheel.

Working program

python
import importlib.util
import sys
import sysconfig
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory

c_source = r"""
#include <Python.h>
#include <limits.h>
static PyObject *total_with_fee(PyObject *self, PyObject *args) {
    long amount;
    if (!PyArg_ParseTuple(args, "l", &amount)) return NULL;
    if (amount < 0 || amount > LONG_MAX - 7) {
        PyErr_SetString(PyExc_ValueError, "amount outside contract");
        return NULL;
    }
    return PyLong_FromLong(amount + 7);
}
static PyMethodDef methods[] = {
    {"total_with_fee", total_with_fee, METH_VARARGS, "Add a fee."},
    {NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
    PyModuleDef_HEAD_INIT, "fee_native", NULL, -1, methods
};
PyMODINIT_FUNC PyInit_fee_native(void) {
    return PyModule_Create(&module);
}
"""
with TemporaryDirectory() as directory:
    source = Path(directory) / "fee_native.c"
    binary = Path(directory) / "fee_native.abi3.so"
    source.write_text(c_source, encoding="utf-8")
    command = ["cc"]
    if sys.platform == "darwin":
        command += ["-bundle", "-undefined", "dynamic_lookup"]
    elif sys.platform.startswith("linux"):
        command += ["-shared", "-fPIC"]
    else:
        raise RuntimeError("this fixture needs a POSIX compiler")
    command += ["-DPy_LIMITED_API=0x030B0000", "-I" + sysconfig.get_path("include"),
                str(source), "-o", str(binary)]
    subprocess.run(command, check=True, capture_output=True, text=True)
    specification = importlib.util.spec_from_file_location("fee_native", binary)
    module = importlib.util.module_from_spec(specification)
    specification.loader.exec_module(module)
    print("total", module.total_with_fee(47))
    try:
        module.total_with_fee(-1)
    except ValueError:
        print("negative_rejected", True)

Output

Output
total 54
negative_rejected True

Costs and limits

A limited interface can reduce rebuilds across compatible interpreter versions, but it does not remove native compilation, packaging, platform wheels, or test cost.

Common Mistakes

  • An abi3 suffix is not proof of stable-ABI compliance by itself.
  • Test imports on the lowest declared Python version, not only the build host.
  • Limited API support is not the same as free-threaded safety.

Connected lessons

Test this contract.

python
limited-api-abi3-build
Storage details