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 wheelThe 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.
| Concept | Meaning |
|---|---|
| Limited API | Header-level source subset exposed during compilation |
| Stable ABI | Binary interface that remains compatible across Python versions |
Py_LIMITED_API | Macro that enables the limited API |
abi3 | Wheel 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 onwardCommon values:
| Value | Minimum CPython |
|---|---|
0x03080000 | 3.8 |
0x03090000 | 3.9 |
0x030A0000 | 3.10 |
0x030B0000 | 3.11 |
0x030C0000 | 3.12 |
0x030D0000 | 3.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_nameLimited 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 directlyExamples:
| Structure | Limited API style |
|---|---|
PyObject | Use object APIs |
PyTypeObject | Use type APIs and heap type specs |
PyFrameObject | Avoid direct frame field access |
PyThreadState | Use documented thread APIs |
PyInterpreterState | Avoid 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 style | Behavior |
|---|---|
| Uppercase macro | Often unchecked, may rely on internals |
| Function API | Safer, 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 finalizationA 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 stateThis avoids many process-global assumptions.
Benefits:
| Feature | Benefit |
|---|---|
| Per-module state | Better interpreter isolation |
| Heap types | Less static runtime state |
| Execution slot | Cleaner initialization |
| Reload handling | More 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 setAlways 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 dictionariesLimited 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 setFor integer status functions:
success = 0
failure = -1 with exception setExample:
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 assumptionsIf 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 pattern | Limited API replacement |
|---|---|
Static PyTypeObject | PyType_Spec heap type |
PyList_GET_ITEM | PyList_GetItem |
PyList_GET_SIZE | PyList_Size |
Direct tp_* access | Type APIs or slots |
| Direct frame fields | Public frame APIs where available |
| Global module state | Per-module state |
Private _Py* helper | Public 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 versionsAvoid 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 operations68.23 Common Mistakes
| Mistake | Result |
|---|---|
Defining Py_LIMITED_API too late | Headers already exposed full API |
| Mixing internal headers | Breaks limited API discipline |
| Publishing non-abi3 wheel | Loses binary portability benefit |
Using static PyTypeObject | Couples to type layout |
| Assuming limited API removes refcounting | Still leaks or crashes |
| Caching interpreter-specific globals | Subinterpreter problems |
| Relying on unchecked macros | ABI 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.