Typing in Cython: Static Types for 10x Speedup
Cython's superpower is static typing. A simple int annotation eliminates polymorphic dispatch; a cdef declaration creates a C variable with no Python overhead. Combine these techniques in tight loops and you'll see 10–100× speedups—all without writing a line of C. This article teaches the type syntax that separates 1.5× speedups from 50× ones.
Cython Type Declarations: def, cdef, and cpdef
Cython offers three function declaration flavors:
def function: Callable from Python; parameters and return types are Python objects by default, unless annotated.
# cython_demo.pyx
def add_python(a, b):
"""Slow: a, b are Python objects."""
return a + b
cdef function: Pure C function, not callable from Python; parameters and return types are C types only.
cdef int add_c(int a, int b):
"""Fast: internal only, pure C."""
return a + b
cpdef function: Callable from both Python and C; has two compiled versions (one that wraps the C version for Python).
cpdef int add_both(int a, int b):
"""Callable from Python and C; slower than cdef, faster than def."""
return a + b
For public functions called from Python, use def with type hints. For internal loops, use cdef.
Type Syntax in Cython
Cython accepts C type syntax. Common types:
cdef int x = 5
cdef float y = 3.14
cdef double z = 2.718
cdef long long big = 9_223_372_036_854_775_807
cdef unsigned int count = 100
cdef char c = 'A'
cdef bint flag = True # bint is C bool (True/False)
cdef object obj = [] # Python object
You can also annotate def function parameters inline:
def distance(double x1, double y1, double x2, double y2):
"""Return Euclidean distance between two points."""
cdef double dx = x2 - x1
cdef double dy = y2 - y1
return (dx * dx + dy * dy) ** 0.5
Typed Arrays and Memoryview: Zero-Copy Buffer Access
One of Cython's biggest wins is memoryview, which gives you zero-copy access to NumPy arrays. Without a memoryview, indexing a NumPy array requires Python object lookups and bounds checking. With a memoryview, you get raw C pointer arithmetic.
# cython_array_sum.pyx
import numpy as np
cimport cython
@cython.boundscheck(False) # Disable bounds checking (faster, unsafe)
@cython.wraparound(False) # Disable negative indexing (faster)
def sum_array(double[:] arr):
"""Sum a 1D array via memoryview; arr is a typed buffer."""
cdef double total = 0.0
cdef int i
cdef int n = arr.shape[0]
for i in range(n):
total += arr[i]
return total
Call it from Python with a NumPy array:
import numpy as np
from cython_array_sum import sum_array
data = np.array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=np.float64)
result = sum_array(data)
print(result) # 15.0
The double[:] syntax means "a typed view of a 1D array of doubles." No copies; no Python object overhead in the loop.
Benchmark pure Python vs Cython:
# Pure Python
def sum_array_python(arr):
return sum(arr)
# Cython (via memoryview)
from cython_array_sum import sum_array
import numpy as np
import timeit
data = np.array(np.random.random(10_000_000), dtype=np.float64)
t_py = timeit.timeit(lambda: sum_array_python(data), number=10)
t_cy = timeit.timeit(lambda: sum_array(data), number=10)
print(f"Python: {t_py:.3f}s")
print(f"Cython: {t_cy:.3f}s")
Output:
Python: 8.234s
Cython: 0.041s
Cython is 200× faster.
2D Arrays and Strided Access
For multidimensional arrays, use double-bracket syntax:
def matrix_sum(double[:, :] matrix):
"""Sum all elements in a 2D array."""
cdef int rows = matrix.shape[0]
cdef int cols = matrix.shape[1]
cdef double total = 0.0
cdef int i, j
for i in range(rows):
for j in range(cols):
total += matrix[i, j]
return total
Pass a 2D NumPy array directly—no conversion needed.
The Importance of cdef Variables
Inside a loop, every variable should be a cdef variable (C type), not a Python object. Compare:
# Slow: total is a Python int
def slow_sum(int[:] arr):
total = 0
for i in range(arr.shape[0]):
total += arr[i]
return total
# Fast: total is a C int
def fast_sum(int[:] arr):
cdef int total = 0
cdef int i
for i in range(arr.shape[0]):
total += arr[i]
return total
The fast version avoids PyObject_Call on every += operation. Benchmark:
Slow: 0.082s
Fast: 0.008s
A 10× difference from one line of cdef.
Typed Containers and Pointers
For lists of C types, use arrays:
cdef int arr[10] # Fixed-size array, allocated on stack
arr[0] = 42
For dynamic arrays or pointers, import from libc.stdlib:
from libc.stdlib cimport malloc, free
cdef int *ptr = <int *>malloc(100 * sizeof(int))
if ptr == NULL:
raise MemoryError()
ptr[0] = 42
ptr[50] = 99
free(ptr)
This raw pointer arithmetic is fast but dangerous—use memoryview for safe buffer access when possible.
Type Inference and Implicit Casting
Cython infers some types:
x = 5 # Cython infers int
y = 3.14 # Cython infers double
z = x + y # Cython infers double (promotion rule)
However, explicit declarations are clearer and more predictable. Never rely on inference in production code.
Key Takeaways
- Use
deffor Python-callable functions,cdeffor internal C-only code, andcpdeffor functions usable from both. - Declare all loop variables with
cdef(e.g.,cdef int i) to avoid Python object overhead. - Use
double[:]anddouble[:, :]memoryview syntax for zero-copy NumPy array access. - Add
@cython.boundscheck(False)and@cython.wraparound(False)to hot loops (after testing correctness). - Type hints enable 10–100× speedups in numerical code—the single biggest win in Cython.
Frequently Asked Questions
What if I forget to declare a variable type?
Cython defaults to Python object for undeclared variables. They'll still work but be slow. Cython prints a warning if you use cimport cython and add @cython.language_level=3; this helps catch missed declarations.
Can I use Python 3 type hints instead of cdef?
Partially. Cython supports PEP 484 annotations (def f(x: int) -> int), but they're hints only—they don't affect performance. For performance, you must use cdef or parameter annotations in def signatures.
What's the performance cost of cpdef vs cdef?
A cpdef function has ~5–10% overhead vs cdef because Cython generates both a C and Python version. Use cpdef only if you need both interfaces; otherwise use cdef internally and def for public entry points.
Can I use memoryview with lists?
No, only with buffer-compatible objects (NumPy arrays, array.array, bytes). For lists, you must extract elements (slow) or convert to a NumPy array first.
How do I handle exceptions in cdef functions?
cdef functions don't propagate Python exceptions by default. To enable exception handling, use except * or except -1:
cdef double divide(double a, double b) except *:
if b == 0:
raise ValueError("Division by zero")
return a / b