Skip to content
advanced Phase 18 · Python Performance

C Extensions & Cython

Speed up Python with C extensions, Cython, and ctypes.

1h 15m
0 problems
Topic Progress 0%

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_INCREF without DECREF grows the heap invisibly — watch sys.getrefcount in tests
  • Portability: extensions must be built per platform/Python ABI; use cibuildwheel to produce manylinux/musllinux/macOS wheels
  • Choose the cheapest tool: array/NumPy vectorization first, Cython second, raw C last; cffi sits 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?

Question 1 options

2. Why does a long-running C extension block other Python threads?

Question 2 options

3. Biggest ctypes crash risk?

Question 3 options

Flashcards

Question

PyArg_ParseTuple

Answer

Parses args tuple into C variables with format codes; returns NULL with an exception on mismatch.

Question

New reference rule

Answer

Py* constructors return a reference you own — DECREF when finished, INCREF when storing an extra copy.

Question

Py_BEGIN_ALLOW_THREADS

Answer

Macro pair releasing the GIL around native compute that touches no Python objects.

Question

ctypes contract

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_ via PyModuleDef; GIL: Py_BEGIN_ALLOW_THREADS; ctypes: set argtypes+restype.

Continue Learning