Skip to content

68. Limited API

Py_LIMITED_API version macros, symbols excluded from the limited API, and building abi3-compatible extensions.

The limited API is a restricted source-level subset of the CPython C API. It is selected at compile time and designed to let extension modules target the stable ABI.

The stable ABI is the binary promise. The limited API is the programming surface used to reach that promise.

extension source code
#define Py_LIMITED_API
limited API declarations
stable ABI compatible binary
abi3 wheel

The limited API makes many CPython implementation details opaque. Extension code uses documented functions and supported type creation mechanisms instead of direct struct access.

68.1 Limited API vs Stable ABI

The two terms are related but distinct.

ConceptMeaning
Limited APIHeader-level source subset exposed during compilation
Stable ABIBinary interface that remains compatible across Python versions
Py_LIMITED_APIMacro that enables the limited API
abi3Wheel tag for stable ABI binaries

The limited API controls what your C compiler sees.

The stable ABI controls what your compiled extension can link against and load with.

68.2 Enabling the Limited API

A source file opts in before including Python.h:

#define Py_LIMITED_API 0x03080000
#define PY_SSIZE_T_CLEAN
#include <Python.h>

The version value means:

use the limited API available from Python 3.8 onward

Common values:

ValueMinimum CPython
0x030800003.8
0x030900003.9
0x030A00003.10
0x030B00003.11
0x030C00003.12
0x030D00003.13

Choose the oldest version you want to support. A lower value gives wider runtime compatibility but fewer available APIs.

68.3 What the Macro Changes

Without Py_LIMITED_API, Python.h exposes the normal CPython API, including many CPython-specific details.

With Py_LIMITED_API, headers hide or restrict implementation details.

Code like this becomes less acceptable:

Py_TYPE(obj)->tp_name

Limited API code should use supported functions:

PyObject *type = PyObject_Type(obj);

The goal is to prevent extension code from depending on layouts that CPython may change.

68.4 Opaque Structures

The limited API treats many structures as opaque.

Opaque means:

you may hold a pointer
you may pass it to API functions
you may not inspect its fields directly

Examples:

StructureLimited API style
PyObjectUse object APIs
PyTypeObjectUse type APIs and heap type specs
PyFrameObjectAvoid direct frame field access
PyThreadStateUse documented thread APIs
PyInterpreterStateAvoid internals

This changes extension design. Code becomes less like “read this struct field” and more like “ask the runtime through a function.”

68.5 Object Access Patterns

Full API code may use direct or macro-based access:

Py_ssize_t n = PyList_GET_SIZE(list);
PyObject *item = PyList_GET_ITEM(list, i);

Limited API code should use checked API functions:

Py_ssize_t n = PyList_Size(list);
PyObject *item = PyList_GetItem(list, i);

The difference matters.

API styleBehavior
Uppercase macroOften unchecked, may rely on internals
Function APISafer, stable ABI friendly

The limited API prefers function calls that preserve ABI boundaries.

68.6 Reference Ownership Still Applies

The limited API does not change ownership rules.

Example:

PyObject *s = PyUnicode_FromString("hello");
if (s == NULL) {
    return NULL;
}

/* use s */

Py_DECREF(s);

PyUnicode_FromString returns a new reference.

Borrowed references remain borrowed:

PyObject *item = PyList_GetItem(list, 0);

Do not decref item unless you first incref it.

The limited API hides layout details. It does not make C memory management automatic.

68.7 Type Creation with PyType_Spec

Limited API code should define heap types using PyType_Spec.

Instead of:

static PyTypeObject PointType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "geometry.Point",
    .tp_basicsize = sizeof(PointObject),
    .tp_new = Point_new,
};

use:

static PyType_Slot Point_slots[] = {
    {Py_tp_new, Point_new},
    {Py_tp_init, Point_init},
    {Py_tp_dealloc, Point_dealloc},
    {Py_tp_methods, Point_methods},
    {0, NULL}
};

static PyType_Spec Point_spec = {
    .name = "geometry.Point",
    .basicsize = sizeof(PointObject),
    .itemsize = 0,
    .flags = Py_TPFLAGS_DEFAULT,
    .slots = Point_slots,
};

Create the type at module initialization:

PyObject *PointType = PyType_FromSpec(&Point_spec);

This avoids direct dependence on PyTypeObject layout.

68.8 Heap Types

Heap types are Python type objects allocated at runtime.

They work better with:

limited API
stable ABI
multi-phase initialization
subinterpreters
per-module state
cleaner finalization

A heap type is still a normal Python type:

p = geometry.Point(1, 2)
print(type(p))

But its C definition is slot-based rather than struct-literal-based.

68.9 Module Creation

Simple module creation remains familiar.

static PyMethodDef methods[] = {
    {"add", add, METH_VARARGS, "Add two integers"},
    {NULL, NULL, 0, NULL}
};

static struct PyModuleDef module = {
    PyModuleDef_HEAD_INIT,
    "demo",
    "Limited API demo",
    -1,
    methods
};

PyMODINIT_FUNC
PyInit_demo(void)
{
    return PyModule_Create(&module);
}

This style is compatible with limited API usage when the functions used are in the supported subset.

68.10 Multi-Phase Initialization

For more robust extension modules, limited API design pairs well with multi-phase initialization.

Conceptual structure:

module definition
module creation slot
module execution slot
create heap types
initialize per-module state

This avoids many process-global assumptions.

Benefits:

FeatureBenefit
Per-module stateBetter interpreter isolation
Heap typesLess static runtime state
Execution slotCleaner initialization
Reload handlingMore predictable lifecycle

68.11 Per-Module State

Limited API extensions should prefer module state over global variables.

Avoid:

static PyObject *DemoError;

Prefer a module state structure:

typedef struct {
    PyObject *DemoError;
    PyObject *PointType;
} demo_state;

The module definition can reserve state:

static struct PyModuleDef module = {
    PyModuleDef_HEAD_INIT,
    "demo",
    "Demo module",
    sizeof(demo_state),
    methods
};

Then retrieve it:

demo_state *st = PyModule_GetState(module);

This is better for subinterpreters because each module instance can have its own state.

68.12 Function Calls

Limited API code can still call Python objects.

PyObject *result = PyObject_CallObject(func, args);

It can also call methods:

PyObject *result =
    PyObject_CallMethod(obj, "close", NULL);

The usual error rule applies:

non-NULL result = success
NULL result = exception set

Always check return values before continuing.

68.13 Attribute Access

Use generic attribute APIs:

PyObject *value =
    PyObject_GetAttrString(obj, "name");

Set:

if (PyObject_SetAttrString(obj, "name", value) < 0) {
    return NULL;
}

These APIs preserve Python semantics:

descriptors
properties
custom __getattribute__
class attributes
instance dictionaries

Limited API code should avoid bypassing these mechanisms through direct layout assumptions.

68.14 Lists, Tuples, and Dicts

You can still use core containers.

List example:

PyObject *list = PyList_New(0);
if (list == NULL) {
    return NULL;
}

PyObject *value = PyLong_FromLong(42);
if (value == NULL) {
    Py_DECREF(list);
    return NULL;
}

if (PyList_Append(list, value) < 0) {
    Py_DECREF(value);
    Py_DECREF(list);
    return NULL;
}

Py_DECREF(value);
return list;

Dict example:

PyObject *dict = PyDict_New();
if (dict == NULL) {
    return NULL;
}

if (PyDict_SetItemString(dict, "answer", PyLong_FromLong(42)) < 0) {
    Py_DECREF(dict);
    return NULL;
}

The second example leaks the temporary integer because it does not store and decref it. Correct version:

PyObject *dict = PyDict_New();
if (dict == NULL) {
    return NULL;
}

PyObject *answer = PyLong_FromLong(42);
if (answer == NULL) {
    Py_DECREF(dict);
    return NULL;
}

if (PyDict_SetItemString(dict, "answer", answer) < 0) {
    Py_DECREF(answer);
    Py_DECREF(dict);
    return NULL;
}

Py_DECREF(answer);
return dict;

Limited API does not protect against ordinary reference bugs.

68.15 Unicode and Bytes

Unicode and bytes APIs are available through stable functions.

Create Unicode:

PyObject *s = PyUnicode_FromString("hello");

Convert to UTF-8 bytes:

PyObject *b = PyUnicode_AsUTF8String(s);

Create bytes:

PyObject *b = PyBytes_FromStringAndSize(buf, len);

Accessing raw internal Unicode layout is not appropriate for limited API code. Use conversion and accessor functions instead.

68.16 Buffers

The buffer protocol can be used through supported APIs.

Consumer pattern:

Py_buffer view;

if (PyObject_GetBuffer(obj, &view, PyBUF_SIMPLE) < 0) {
    return NULL;
}

/* use view.buf and view.len */

PyBuffer_Release(&view);

This remains useful for stable ABI extensions that wrap native libraries expecting memory ranges.

68.17 Capsules

Capsules fit well with limited API design.

They allow one extension to export a stable C function table:

PyObject *capsule =
    PyCapsule_New(&Demo_API, "demo._C_API", NULL);

A consumer imports and validates:

DemoAPI *api =
    PyCapsule_GetPointer(capsule, "demo._C_API");

For stable extension ecosystems, include version and size fields in the exported table.

typedef struct {
    int version;
    size_t size;
    PyObject *(*make_value)(long);
} DemoAPI;

68.18 Error Handling

Limited API error handling follows the normal C API convention.

For functions returning PyObject *:

success = new reference
failure = NULL with exception set

For integer status functions:

success = 0
failure = -1 with exception set

Example:

PyObject *obj = PyObject_CallObject(func, args);
if (obj == NULL) {
    return NULL;
}

Never continue after a failed API call unless you intentionally handle or clear the exception.

68.19 What to Avoid

Limited API code should avoid:

Include/cpython/
Include/internal/
private _Py* functions
direct PyTypeObject field access
direct PyFrameObject field access
unchecked macros that depend on layout
global borrowed references
static mutable Python object state
version-specific bytecode assumptions

If your code requires these, it probably needs the full CPython API.

68.20 Common Porting Changes

When converting full API code to limited API code, changes often look like this:

Full API patternLimited API replacement
Static PyTypeObjectPyType_Spec heap type
PyList_GET_ITEMPyList_GetItem
PyList_GET_SIZEPyList_Size
Direct tp_* accessType APIs or slots
Direct frame fieldsPublic frame APIs where available
Global module statePer-module state
Private _Py* helperPublic API equivalent or local code

Some code ports cleanly. Some code needs architectural change.

68.21 Build Configuration

A build must define the macro and request limited API output.

Setuptools example:

from setuptools import Extension, setup

setup(
    name="demo",
    ext_modules=[
        Extension(
            "demo",
            ["demo.c"],
            py_limited_api=True,
            define_macros=[
                ("Py_LIMITED_API", "0x03080000"),
            ],
        )
    ],
)

The wheel must also use an abi3 tag. Build tooling needs to be configured correctly, not only the C source.

68.22 Practical Decision Rule

Use the limited API when your extension:

wraps a C library
does coarse-grained native work
does not inspect interpreter internals
does not rely on private APIs
can define heap types
benefits from one wheel across Python versions

Avoid it when your extension:

needs frame internals
modifies type internals directly
uses private CPython functions
depends on exact object layout
implements a low-level profiler or debugger
needs maximum speed for tiny object operations

68.23 Common Mistakes

MistakeResult
Defining Py_LIMITED_API too lateHeaders already exposed full API
Mixing internal headersBreaks limited API discipline
Publishing non-abi3 wheelLoses binary portability benefit
Using static PyTypeObjectCouples to type layout
Assuming limited API removes refcountingStill leaks or crashes
Caching interpreter-specific globalsSubinterpreter problems
Relying on unchecked macrosABI fragility

68.24 Chapter Summary

The limited API is the source-level subset of CPython’s C API used to build stable ABI extensions. It is enabled with Py_LIMITED_API before including Python.h.

It hides many implementation details and encourages extension code to use function APIs, heap types, module state, capsules, buffer interfaces, and documented runtime operations.

The limited API trades some power and sometimes some speed for binary portability. It is a good fit for native library wrappers, binary parsers, codecs, database adapters, and extensions that do most of their work outside Python object internals.