A tour of the C API
Read and write a small CPython extension in C: PyObject pointers, owned and borrowed references, error returns, argument parsing, a method table and the new PyModExport export hook of Python 3.15. Then build it for the stable ABIs abi3 and abi3t.
What you will learn
- Explain what a PyObject pointer is and what the object header holds
- Apply the rules for new, borrowed and stolen references
- Follow the C API error convention of NULL or -1 with an exception set
- Parse positional and keyword arguments, and release references on every error path
- Define a module with PySlot entries and a PyModExport_ hook
- Choose between the version-specific ABI, abi3 and abi3t
On this page
Every lesson in this module has pointed at C: the reference counts of memory management, the Py_mod_gil slot of free-threading internals, the extension loaders of the import system. This lesson reads and writes a complete small extension, tally.c, built up over several blocks. You need to read C pointers and know what malloc is for; if you do not, the native extensions lesson covers ctypes, cffi and Cython, which spare you this level. You need a C compiler and the Python headers to build it, but you can follow the code without one.
PyObject
In C, every Python object is reached through a PyObject *. The object starts with a header: on the default build, its reference count and a pointer to its type; on the free-threaded build, the larger header from the previous lessons. Everything else depends on the type. A PyLongObject continues with digits, a PyListObject with an array of PyObject * items.
You never read the header fields directly. Use Py_TYPE(op), Py_INCREF(op), Py_DECREF(op) and Py_NewRef(op) (increment and return), which do the right thing on every build, including for immortal objects.
Owning references
The C API speaks of owning references, not objects. If you own a reference, you must eventually release it with Py_DECREF, or hand that duty to someone else. Each function’s documentation says which of three things happens:
- New reference: the function hands you ownership. Almost every function that returns an object does this, including all
PyObject_*,PyNumber_*,PySequence_*andPyMapping_*calls. - Borrowed reference: you may use the object, but you do not own it and must not release it.
PyList_GetItemis the classic case. It is only safe while something else keeps the object alive, which on the free-threaded build another thread can break, so preferPyList_GetItemRefandPyDict_GetItemRef, which return new references. - Stolen reference: you pass an object and the function takes over your ownership.
PyList_SetItemandPyTuple_SetItemdo this.
Errors follow one convention: a function that fails sets an exception and returns NULL (for pointer results) or -1 (for int results). Your function must check every call, release what it owns, and return NULL itself, which makes Python raise the exception.
A function in C
count_true(iterable) counts the truthy items. Notice how each new reference is released on every path:
#include <Python.h>
/* tally.count_true(iterable) -> int: how many items are truthy. */static PyObject *count_true(PyObject *module, PyObject *iterable){ PyObject *it = PyObject_GetIter(iterable); /* new reference */ if (it == NULL) { return NULL; /* exception is set */ } long total = 0; PyObject *item; while ((item = PyIter_Next(it)) != NULL) { /* new reference */ int truth = PyObject_IsTrue(item); Py_DECREF(item); /* done with item */ if (truth < 0) { Py_DECREF(it); return NULL; } total += truth; }PyIter_Next returns NULL both at the end and on an error, so the end of the loop has to ask which one it was:
Py_DECREF(it); if (PyErr_Occurred()) { /* iteration failed */ return NULL; } return PyLong_FromLong(total); /* new reference */}The returned int is a new reference, and ownership passes to the caller, the interpreter.
Arguments and error paths
scale(seq, factor=2.0) takes two arguments, the second optional and allowed as a keyword, and returns a new list. With METH_VARARGS | METH_KEYWORDS, the function receives a tuple and a dict, and PyArg_ParseTupleAndKeywords unpacks them: O stores an object, | starts the optional ones, d converts to a C double, and the name after : goes into error messages.
/* tally.scale(seq, factor=2.0) -> list of floats: each item * factor. */static PyObject *scale(PyObject *module, PyObject *args, PyObject *kwargs){ static char *kwlist[] = {"seq", "factor", NULL}; PyObject *seq; double factor = 2.0; /* default if not passed */ if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|d:scale", kwlist, &seq, &factor)) { return NULL; /* TypeError is set */ } PyObject *it = PyObject_GetIter(seq); if (it == NULL) { return NULL; } PyObject *result = PyList_New(0); if (result == NULL) { Py_DECREF(it); return NULL; }From here the function owns two references, it and result, so every failure must release both. C code does that with one error: label:
PyObject *item; while ((item = PyIter_Next(it)) != NULL) { double x = PyFloat_AsDouble(item); Py_DECREF(item); if (x == -1.0 && PyErr_Occurred()) { goto error; /* not a number */ } PyObject *value = PyFloat_FromDouble(x * factor); if (value == NULL || PyList_Append(result, value) < 0) { Py_XDECREF(value); goto error; } Py_DECREF(value); /* the list holds its own */ } if (PyErr_Occurred()) { goto error; } Py_DECREF(it); return result;
error: /* one exit for every failure */ Py_DECREF(it); Py_DECREF(result); return NULL;}PyFloat_AsDoublereturns-1.0both for a real-1.0and on error, so onlyPyErr_Occurred()tells them apart.PyList_Appenddoes not steal, sovalueis released after it, whilePyList_SetItemwould have taken it over.Py_XDECREFacceptsNULL, which covers a failedPyFloat_FromDouble.
METH_FASTCALL passes the arguments as a C array, with no tuple to build. The parser for that array, PyArg_ParseArray, is new in 3.15 and not in the Limited API, so tally uses the tuple form, which also builds for abi3t below.
The module: slots and an export hook
A module is described by an array of PySlot entries, and the shared library exports one function, PyModExport_<name>, that returns it (PEP 793):
static PyMethodDef tally_methods[] = { {"count_true", count_true, METH_O, "Count the truthy items."}, {"scale", (PyCFunction)(void (*)(void))scale, METH_VARARGS | METH_KEYWORDS, "Multiply every item by factor."}, {NULL, NULL, 0, NULL}};
PyABIInfo_VAR(abi_info);
static PySlot tally_slots[] = { PySlot_STATIC_DATA(Py_mod_abi, &abi_info), PySlot_STATIC_DATA(Py_mod_name, "tally"), PySlot_STATIC_DATA(Py_mod_doc, "Count things, in C."), PySlot_STATIC_DATA(Py_mod_methods, tally_methods), PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), PySlot_END};
PyMODEXPORT_FUNC PyModExport_tally(void);
PyMODEXPORT_FUNCPyModExport_tally(void){ return tally_slots;}METH_Omeans the C function takes exactly one argument.METH_NOARGS,METH_VARARGSandMETH_FASTCALLcover the other shapes. The table’s field is a plainPyCFunction, soscale, which takes a third argument, needs the cast.Py_mod_abiwithPyABIInfo_VARlets Python refuse an extension compiled for an incompatible ABI instead of crashing.Py_mod_gilis the free-threading promise from the GIL lesson. Neither function keeps shared state, so the promise holds.- Everything is
staticexcept the hook, so nothing clashes with other extensions.
Before 3.15, the export was PyInit_<name>, returning a PyModuleDef. It still works, and is now soft deprecated: no new features, no plans for removal. If a library exports both, PyInit_<name> is ignored, so you can ship one source that supports older versions with PyInit_ and 3.15 with PyModExport_. The extension modules reference has both forms.
The hook name is derived from the module name, with a Punycode form for non-ASCII names:
def hook_name(name): try: suffix = b"_" + name.encode("ascii") except UnicodeEncodeError: suffix = b"U_" + name.encode("punycode").replace(b"-", b"_") return (b"PyModExport" + suffix).decode("ascii")
for module in ["spam", "tally", "café"]: print(f"{module:<6} {hook_name(module)}")spam PyModExport_spamtally PyModExport_tallycafé PyModExportU_caf_dmaBuilding and importing
Real projects build with meson-python, scikit-build-core or setuptools. For a one-file experiment on Linux, the compiler is enough:
$ gcc -shared -fPIC $(python3.15-config --includes) tally.c \ -o tally$(python3.15-config --extension-suffix)$ python3.15 -c "import tally; print(tally.count_true([0, 1, 'a', '', None, 3]))"3$ python3.15 -c "import tally; tally.count_true(5)"Traceback (most recent call last): ...TypeError: 'int' object is not iterableThe TypeError was set by PyObject_GetIter; count_true only passed the NULL on. scale behaves like a Python function, keyword argument and default included, and each failure arrives as an ordinary exception:
$ python3.15 -c "import tally; print(tally.scale([1, 2.5, True]))"[2.0, 5.0, 2.0]$ python3.15 -c "import tally; print(tally.scale(range(3), factor=0.5))"[0.0, 0.5, 1.0]$ python3.15 -c "import tally; tally.scale([1, 'two'])"Traceback (most recent call last): ...TypeError: must be real number, not str$ python3.15 -c "import tally; tally.scale()"Traceback (most recent call last): ...TypeError: scale() missing required argument 'seq' (pos 1)The first message comes from PyFloat_AsDouble, the second from the parser, which used the name after :.
Stable ABIs: abi3 and abi3t
A normal build, tally.cpython-315-x86_64-linux-gnu.so, works on 3.15 only. Defining a target ABI before including Python.h limits you to the Limited API, and in exchange the binary works on that version and every later one:
Py_LIMITED_APItargets abi3, the stable ABI for builds with a GIL (since 3.2).Py_TARGET_ABI3Ttargets abi3t, the stable ABI for free-threaded builds (PEP 803). Defining both gives one binary for both kinds of build, which the stable ABI documentation recommends naming with theabi3ttag.
Each takes a version in hex, 0x030f0000 for 3.15:
$ gcc -shared -fPIC $(python3.15-config --includes) \ -DPy_LIMITED_API=0x030f0000 -DPy_TARGET_ABI3T=0x030f0000 \ tally.c -o tally.abi3t.so$ python3.15 -c "import tally; print(tally.count_true([1, 0, 2]))"2$ python3.15t -c "import sys, tally; print(tally.count_true([1, 0, 2]), sys._is_gil_enabled())"2 FalseThe suffixes each interpreter accepts, in the order it tries them:
$ python3.15 -c "import importlib.machinery as m; print(m.EXTENSION_SUFFIXES)"['.cpython-315-x86_64-linux-gnu.so', '.abi3-x86_64-linux-gnu.so', '.abi3.so', '.abi3t-x86_64-linux-gnu.so', '.abi3t.so', '.so']$ python3.15t -c "import importlib.machinery as m; print(m.EXTENSION_SUFFIXES)"['.cpython-315t-x86_64-linux-gnu.so', '.abi3t-x86_64-linux-gnu.so', '.abi3t.so', '.so']The Limited API costs some speed: macros such as PyList_GET_ITEM are unavailable, and calls that would be inlined become function calls. You can ship both: a faster version-specific build alongside a stable-ABI one as the fallback.
When not to write C by hand
Write raw C API code to learn how CPython works, to maintain an existing extension, or for a small hot function with no dependencies. For new bindings to a C library, ctypes or cffi need no compiler at install time. For new compiled code, Cython or Rust with PyO3 manage reference counts for you. A single forgotten Py_DECREF is a leak; a single extra one is a crash, possibly much later.
Common pitfalls
Releasing a borrowed reference. Py_DECREF(PyList_GetItem(...)) frees an object the list still uses.
Missing a release on an error path. Every early return NULL must first release what you own.
Ignoring return values. PyObject_IsTrue returns -1 on error; treating that as “true” hides exceptions.
Declaring Py_MOD_GIL_NOT_USED for code with static state. That is a data race waiting for the free-threaded build.
Write a module hello with one function, greet(name), that returns "Hello, NAME!" and raises TypeError with the message name must be a str for anything that is not a str. Use a PyModExport_hello hook, declare Py_mod_gil, and build it as hello.abi3t.so. Expected results:
$ python3.15 -c "import hello; print(hello.greet('Ada'))"Hello, Ada!$ python3.15 -c "import hello; hello.greet(3)"Traceback (most recent call last): ...TypeError: name must be a str$ python3.15t -c "import sys, hello; print(hello.greet('Ada'), sys._is_gil_enabled())"Hello, Ada! FalseShow solution
#include <Python.h>
static PyObject *greet(PyObject *module, PyObject *name){ if (!PyUnicode_Check(name)) { PyErr_SetString(PyExc_TypeError, "name must be a str"); return NULL; } return PyUnicode_FromFormat("Hello, %U!", name); /* new reference */}
static PyMethodDef hello_methods[] = { {"greet", greet, METH_O, "Return a greeting."}, {NULL, NULL, 0, NULL}};PyABIInfo_VAR(abi_info);
static PySlot hello_slots[] = { PySlot_STATIC_DATA(Py_mod_abi, &abi_info), PySlot_STATIC_DATA(Py_mod_name, "hello"), PySlot_STATIC_DATA(Py_mod_methods, hello_methods), PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED), PySlot_END};
PyMODEXPORT_FUNC PyModExport_hello(void);
PyMODEXPORT_FUNCPyModExport_hello(void){ return hello_slots;}Build with the abi3t command above, replacing tally with hello. %U in PyUnicode_FromFormat inserts a str object. No reference is released because the function owns none: name is borrowed from the caller.
A colleague’s version of count_true passes every test, but a long-running service that calls it grows without limit. This check, run against their build, prints 1000; against a correct build it prints 0:
import sys, tally
token = object()items = [token] * 1000before = sys.getrefcount(token)tally.count_true(items)print(sys.getrefcount(token) - before)Their loop is below. Find the bug and fix it.
while ((item = PyIter_Next(it)) != NULL) { int truth = PyObject_IsTrue(item); if (truth < 0) { Py_DECREF(it); return NULL; } total += truth; }Show solution
PyIter_Next returns a new reference, and the loop never releases it, so every item gains one reference per call: 1000 items, 1000 extra. Release the item as soon as you are done with it, before the error check so that the error path does not leak either:
while ((item = PyIter_Next(it)) != NULL) { int truth = PyObject_IsTrue(item); Py_DECREF(item); if (truth < 0) { Py_DECREF(it); return NULL; } total += truth; }The test used object() on purpose: an immortal object such as None or True would hide the leak, because its count never changes.
A wheel directory contains tally.abi3.so, tally.abi3t.so and tally.cpython-315-x86_64-linux-gnu.so. Using the suffix lists above, predict which file import tally loads on python3.15 and on python3.15t, and which file a free-threaded interpreter can never load.
Show solution
python3.15 tries .cpython-315-x86_64-linux-gnu.so first, so it loads the version-specific build. python3.15t has neither that suffix nor .abi3.so in its list, so it loads tally.abi3t.so. It can never load tally.abi3.so: abi3 is only compatible with builds that have a GIL, and free-threaded interpreters recognise only the abi3t tag. That is why a project that supports both builds with one binary names it with the abi3t tag.
Summary
- Every object is a
PyObject *with a header you access through macros. - Know, for every call, whether you get a new or a borrowed reference, and whether you pass one in to be stolen. Release what you own on every path.
- Failures set an exception and return
NULLor-1; pass them on, releasing what you own, often through oneerror:label. PyArg_ParseTupleAndKeywordsturns a tuple and a dict into C values, with optional and keyword arguments.- Modules are
PySlotarrays returned by aPyModExport_<name>hook;PyInit_is soft deprecated. Py_LIMITED_APIbuilds abi3;Py_TARGET_ABI3Tbuilds abi3t, which loads on both kinds of build.- This completes the CPython internals module.