The CPython C API
The CPython C API
A C extension compiles against Python.h and exports one initializer:
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject *sum_range(PyObject *self, PyObject *args) {
long n;
if (!PyArg_ParseTuple(args, "l", &n)) return NULL;
long s = 0;
for (long i = 0; i <= n; i++) s += i;
return PyLong_FromLong(s);
}
static PyMethodDef methods[] = {
{"sum_range", sum_range, METH_VARARGS, "sum 0..n"},
{NULL, NULL, 0, NULL}
};
PyMODINIT_FUNC PyInit_fastmod(void) {
return PyModuleDef_Init(&(PyModuleDef){PyModuleDef_HEAD_INIT,
"fastmod", NULL, -1, methods});
}
Rules that prevent crashes: every Py* call returning an object hands you a new reference you must Py_DECREF; NULL plus a set exception is the error path. Ship it with setuptools Extension("fastmod", ["fastmod.c"]).
Cython and Release Builds
Cython and Release Builds
Cython turns annotated .pyx files into C — most of the speed, fraction of the pain:
# fast.pyx
def clamp(double x, double lo, double hi):
cdef double r = x if x < hi else hi # typed locals skip object boxing
return r if r > lo else lo
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("fast.pyx"))
Typed variables, cdef functions taking C types, and with nogil: blocks (for pure compute after references are released) remove interpreter dispatch per operation. Compile with -O3; profile first — NumPy vectorization often wins without any C.
The GIL: native code still holds it, so a CPU-bound C function blocks other threads. Release it around pure computation with Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS, but only when no Python objects are touched.
ctypes, Pitfalls, and Interviews
ctypes, Pitfalls, and Interviews
No compiler needed for existing shared libraries:
import ctypes
lib = ctypes.CDLL("libz.so") # or zlib via cdll.LoadLibrary
lib.compressBound.argtypes = [ctypes.c_ulong]
lib.compressBound.restype = ctypes.c_ulong # declare both — defaults assume int
Failure modes to name in interviews:
- Missing argtypes/restype: default int truncation of pointers and 64-bit returns → segfaults, not exceptions
- Refcount leaks in C:
Py_INCREFwithoutDECREFgrows the heap invisibly — watchsys.getrefcountin tests - Portability: extensions must be built per platform/Python ABI; use
cibuildwheelto produce manylinux/musllinux/macOS wheels - Choose the cheapest tool:
array/NumPy vectorization first, Cython second, raw C last;cffisits between ctypes and the C API when you need both speed and sanity
Quiz
1. What must you do with a new reference returned by PyLong_FromLong after storing nothing?
2. Why does a long-running C extension block other Python threads?
3. Biggest ctypes crash risk?
Flashcards
Question
PyArg_ParseTuple
Click to reveal answer
Answer
Parses args tuple into C variables with format codes; returns NULL with an exception on mismatch.
Question
New reference rule
Click to reveal answer
Answer
Py* constructors return a reference you own — DECREF when finished, INCREF when storing an extra copy.
Question
Py_BEGIN_ALLOW_THREADS
Click to reveal answer
Answer
Macro pair releasing the GIL around native compute that touches no Python objects.
Question
ctypes contract
Click to reveal answer
Answer
Always set .argtypes and .restype — otherwise default int interpretation can segfault.
Revision Notes
Key Takeaways
- 1. C extensions buy speed at the cost of manual reference counting and per-platform builds
- 2. Cython is the pragmatic middle path; profile before crossing into C at all
- 3. Declare ctypes signatures fully, and release the GIL for CPU-bound native work
Interview Tips
- • Explain the new-reference contract and a concrete leak scenario before showing any C code
- • Name the full optimization ladder: algorithm → vectorized libs → Cython → C with GIL release
Cheat Sheet
PyArg_ParseTuple fmt "l"; new refs → Py_DECREF; PyInit_