Native extensions — Cython, Numba, ctypes, PyO3
When pure Python is too slow even after algorithm and stdlib optimization, drop to native code. Each tool fits a different niche.
Cython — Python superset that compiles to C
Annotate Python code with C-like types; Cython generates a compiled .so/.pyd:
# fib.pyx
def fib(int n):
cdef int a = 0, b = 1, i
for i in range(n):
a, b = b, a + b
return a
Compile via setup.py or pyproject.toml with the Cython build backend. Result is a regular Python module that’s 10-1000x faster on tight loops with C types.
When to use: existing performance-critical Python code that you can annotate; numerical pipelines where vectorization isn’t enough.
Trade-off: build complexity (C compiler required), .pyx files aren’t pure Python.
Numba — JIT for numerical Python
Decorator-based; works on NumPy-heavy code:
from numba import jit
import numpy as np
@jit(nopython=True)
def sum_squares(arr):
s = 0.0
for x in arr:
s += x * x
return s
sum_squares(np.arange(1_000_000.0)) # JIT compiles on first call, then fast
nopython=True (alias @njit) is required for real speed — falls back to slow mode otherwise.
When to use: heavy numerical loops over NumPy arrays where vectorization isn’t expressive enough.
Limitations: only supports a subset of Python (no arbitrary dicts of objects, limited string handling, no exceptions inside @jit functions).
mypyc — compile typed Python
Used by mypy itself for ~4x speedup. Compiles type-annotated .py files to C extensions:
mypyc my_module.py
When to use: a fully type-annotated Python codebase; you want speed without rewriting.
Limitations: type annotations must be precise; some Python features have caveats; not as widely deployed as Cython.
ctypes — call C libraries from Python
For wrapping existing C libraries without writing C extension code:
import ctypes
libc = ctypes.CDLL("libc.so.6")
libc.printf(b"Hello %s\n", b"World")
# Or call your own .so:
mylib = ctypes.CDLL("./libmath.so")
mylib.add.argtypes = [ctypes.c_int, ctypes.c_int]
mylib.add.restype = ctypes.c_int
print(mylib.add(2, 3))
When to use: integrating with system libraries, vendor SDKs distributed as .so/.dll.
Trade-off: type marshaling is manual and error-prone; no help with memory safety.
cffi is a higher-level alternative.
PyO3 — Rust extensions
State of the art for new performance-critical Python libraries:
use pyo3::prelude::*;
#[pyfunction]
fn sum_squares(arr: Vec<f64>) -> f64 {
arr.iter().map(|x| x * x).sum()
}
#[pymodule]
fn fastmath(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(sum_squares, m)?)?;
Ok(())
}
Use maturin to build a wheel. Modern Python libraries increasingly use Rust + PyO3: tokenizers (HuggingFace), polars, pydantic-core (v2), ruff, uv.
When to use: building a new library where Python is too slow; you (or your team) know Rust.
Trade-off: requires Rust toolchain; longer compile times than Cython.
CPython C API — the original approach
Write a .c file that uses Python.h:
#include <Python.h>
static PyObject* myfunc(PyObject* self, PyObject* args) {
int x;
if (!PyArg_ParseTuple(args, "i", &x)) return NULL;
return PyLong_FromLong(x * 2);
}
static PyMethodDef Methods[] = {
{"myfunc", myfunc, METH_VARARGS, "Multiply by 2."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef moduledef = {
PyModuleDef_HEAD_INIT, "mymodule", NULL, -1, Methods
};
PyMODINIT_FUNC PyInit_mymodule(void) {
return PyModule_Create(&moduledef);
}
Don’t choose this for new projects. Use Cython or PyO3 instead. The raw C API is verbose, error-prone, and unstable across Python versions.
Decision tree
Need to speed up Python code?
├── Pure-Python CPU loops?
│ ├── Numerical / NumPy-heavy?
│ │ ├── Vectorize with NumPy ← simplest
│ │ ├── Numba @jit ← if loops won't vectorize
│ │ └── Cython ← maximum control
│ └── General logic?
│ ├── Cython ← most mature
│ └── mypyc ← if fully typed
├── Wrap existing C lib?
│ ├── ctypes ← simple cases
│ └── cffi ← cleaner for nontrivial APIs
└── Building new perf-critical library?
├── PyO3 + Rust ← modern, safe, fast
└── Cython ← if Rust isn't available
Interview angle
- “When would you write a C extension instead of using Python?” → Pure-Python CPU bottleneck, after profiling, and after stdlib + NumPy aren’t enough.
- “Difference between Cython and Numba?” → Cython is AOT compilation of annotated Python to C; Numba is a JIT for numerical Python (NumPy-focused).
- “Why are libraries like Polars and Ruff written in Rust?” → Memory safety + performance + good Python interop via PyO3.