Skip to main content

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 def for Python-callable functions, cdef for internal C-only code, and cpdef for functions usable from both.
  • Declare all loop variables with cdef (e.g., cdef int i) to avoid Python object overhead.
  • Use double[:] and double[:, :] 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

Further Reading