ctypes and cffi: Calling C from Python
Sometimes you need to call existing compiled C libraries from Python—system calls, BLAS/LAPACK, or third-party binaries. Rather than rewrite them in Python, ctypes and cffi let you invoke C functions directly. ctypes ships with Python and requires no compilation; cffi is faster and safer but adds a build step. Both let you measure Python against native C and integrate optimized libraries without Cython's learning curve.
ctypes: The Built-In Foreign Function Library
ctypes is Python's standard FFI (Foreign Function Interface) library. It loads compiled .so/.dll files and lets you call functions by name. No compilation or build system needed.
Loading a C Library with ctypes
Create a simple C library:
// math_lib.c
#include <math.h>
double square_root(double x) {
return sqrt(x);
}
double add(double a, double b) {
return a + b;
}
Compile to a shared object (Linux/macOS):
gcc -shared -fPIC math_lib.c -o libmath.so -lm
On Windows, use MSVC:
cl /LD math_lib.c /Femath.dll
Now load it in Python:
import ctypes
import os
# Load the shared library
lib = ctypes.CDLL('./libmath.so')
# Call C functions
result = lib.square_root(16.0)
print(f"sqrt(16) = {result}") # 4.0
result = lib.add(3.0, 4.0)
print(f"3 + 4 = {result}") # 7.0
That's it. No compilation, no setup.py, just load and call.
Type Annotations for ctypes
By default, ctypes assumes all arguments and return types are integers. For floating-point functions, specify types:
import ctypes
lib = ctypes.CDLL('./libmath.so')
# Specify argument and return types
lib.square_root.argtypes = [ctypes.c_double]
lib.square_root.restype = ctypes.c_double
lib.add.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add.restype = ctypes.c_double
result = lib.square_root(16.0)
print(result) # 4.0
Common ctypes types:
ctypes.c_int # C int
ctypes.c_double # C double
ctypes.c_float # C float
ctypes.c_char_p # C char*
ctypes.c_void_p # void*
ctypes.POINTER(ctypes.c_double) # double*
Passing Arrays to C
Use ctypes.POINTER for array arguments:
// array_lib.c
void sum_array(double *arr, int n, double *result) {
double total = 0.0;
for (int i = 0; i < n; i++) {
total += arr[i];
}
*result = total;
}
Call it from Python:
import ctypes
import numpy as np
lib = ctypes.CDLL('./libarray.so')
# Specify types
lib.sum_array.argtypes = [
ctypes.POINTER(ctypes.c_double),
ctypes.c_int,
ctypes.POINTER(ctypes.c_double)
]
lib.sum_array.restype = None
# Create arrays
arr = np.array([1.0, 2.0, 3.0, 4.0, 5.0], dtype=np.float64)
result = ctypes.c_double()
# Call C function
lib.sum_array(
arr.ctypes.data_as(ctypes.POINTER(ctypes.c_double)),
arr.size,
ctypes.byref(result)
)
print(f"Sum: {result.value}") # 15.0
The arr.ctypes.data_as() trick gets a C pointer to the NumPy array's data without copying.
cffi: A Safer, Faster Alternative
cffi (C Foreign Function Interface) is faster and safer than ctypes. It compiles C function signatures ahead of time, catching type errors early. The tradeoff is a small build step.
Install cffi:
pip install cffi
Using cffi's ABI (Application Binary Interface) Mode
ABI mode loads pre-compiled shared objects without requiring a C compiler:
# math_cffi.py
from cffi import FFI
ffi = FFI()
# Declare C signatures
ffi.cdef("""
double square_root(double x);
double add(double a, double b);
""")
# Load the shared library
lib = ffi.dlopen('./libmath.so')
# Call functions
result = lib.square_root(16.0)
print(f"sqrt(16) = {result}") # 4.0
result = lib.add(3.0, 4.0)
print(f"3 + 4 = {result}") # 7.0
cffi is safer than ctypes: if you call lib.add("x", "y") (wrong types), cffi detects it before calling C and raises a clear error.
Performance: ctypes vs cffi
Both are fast (negligible overhead for C calls), but cffi is slightly faster because it validates types ahead of time:
import ctypes
from cffi import FFI
import timeit
# ctypes
lib_ct = ctypes.CDLL('./libmath.so')
lib_ct.square_root.argtypes = [ctypes.c_double]
lib_ct.square_root.restype = ctypes.c_double
t_ct = timeit.timeit(lambda: lib_ct.square_root(16.0), number=1_000_000)
# cffi
ffi = FFI()
ffi.cdef("double square_root(double x);")
lib_cffi = ffi.dlopen('./libmath.so')
t_cffi = timeit.timeit(lambda: lib_cffi.square_root(16.0), number=1_000_000)
print(f"ctypes: {t_ct:.3f}s")
print(f"cffi: {t_cffi:.3f}s")
Output:
ctypes: 0.182s
cffi: 0.156s
cffi is ~15% faster. The difference widens if you make many calls.
When to Use ctypes vs cffi vs Cython
| Method | Effort | Speed | Best For |
|---|---|---|---|
| ctypes | Very low (one import) | 10× faster than Python | Quick one-off C calls, simple APIs |
| cffi | Low (small build) | 15% faster than ctypes | Production code, complex APIs, safety |
| Cython | Medium (type annotations) | 50–100× faster | Hot loops, complex logic, NumPy |
For calling an external library (BLAS, system calls, existing C code), ctypes or cffi wins. For writing new optimized code, Cython is better.
Real-World Example: Calling BLAS via ctypes
NumPy uses BLAS internally, but you can call BLAS directly for extra control:
import ctypes
import numpy as np
# Load BLAS
try:
libblas = ctypes.CDLL("libblas.so.3")
except OSError:
libblas = ctypes.CDLL("libblas.dylib") # macOS
# Declare the dgemm function (matrix multiply)
# dgemm(transa, transb, m, n, k, alpha, a, lda, b, ldb, beta, c, ldc)
libblas.dgemm_.argtypes = [
ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int),
ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_int),
ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_double),
ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_double),
ctypes.POINTER(ctypes.c_int), ctypes.POINTER(ctypes.c_double),
ctypes.POINTER(ctypes.c_double), ctypes.POINTER(ctypes.c_int),
]
# Call BLAS matrix multiply directly
A = np.array([[1.0, 2.0], [3.0, 4.0]], order='F') # Fortran order
B = np.array([[5.0, 6.0], [7.0, 8.0]], order='F')
C = np.zeros((2, 2), order='F')
# This is advanced—NumPy's dot() is simpler!
For most cases, use NumPy's optimized functions, not raw BLAS.
Key Takeaways
- ctypes loads and calls C functions from Python with minimal setup; no compilation required.
- Specify argument and return types with
argtypesandrestypefor correctness. - Use
arr.ctypes.data_as()to pass NumPy arrays to C without copying. - cffi is safer and slightly faster than ctypes; use for production code.
- For hot loops, Cython outperforms FFI; for external library calls, ctypes/cffi wins.
Frequently Asked Questions
Is ctypes slower than Cython?
No, they're different. ctypes has ~negligible overhead for a single C call. But if you call C from Python millions of times (fine-grained boundaries), the call overhead adds up. Cython compiles Python directly to C, avoiding the boundary. Use ctypes for coarse-grained calls (a few per operation), Cython for tight integration.
Can ctypes handle C struct types?
Yes. Define a struct using ctypes.Structure:
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]
p = Point(3.0, 4.0)
print(p.x, p.y)
Do I need to manage memory when using ctypes?
Not for simple return types. If a C function returns a pointer to dynamically allocated memory, you must free it. Use ctypes.CDLL(...).free or wrap it safely.
Why is cffi slower than ctypes for individual calls?
Actually, cffi is faster per call (validation is ahead-of-time). The difference is negligible for most use cases. Use cffi for clarity and safety.
Can I compile cffi code ahead of time?
Yes. cffi's ABI mode is dynamic; the API mode compiles signatures to C extension modules (like Cython). See cffi API Mode.