Skip to main content

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​

MethodEffortSpeedBest For
ctypesVery low (one import)10× faster than PythonQuick one-off C calls, simple APIs
cffiLow (small build)15% faster than ctypesProduction code, complex APIs, safety
CythonMedium (type annotations)50–100× fasterHot 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 argtypes and restype for 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.

Further Reading​