There are situations where you need to leverage C code from Python, whether for performance-critical computations, reusing an existing C library, or interfacing with system-level APIs. Python's built-in ctypes module makes this possible by providing a foreign function interface (FFI) that lets you load shared libraries and call C functions directly from Python.
This guide walks through the entire process: writing C code, compiling it into a shared library, and calling it from Python.
Why Call C from Python?
- Performance: C is significantly faster than Python for CPU-intensive operations.
- Reuse existing libraries: leverage well-tested C libraries without rewriting them.
- Hardware/system access: interact with low-level system APIs written in C.
- Legacy code: integrate existing C codebases into Python applications.
Step 1: Write the C Function
Create a C source file with the function(s) you want to call from Python.
mathutils.c:
// Check if a number is a power of 2
int is_power_of_two(int num) {
if (num <= 0)
return 0;
return (num & (num - 1)) == 0 ? 1 : 0;
}
// Calculate factorial
long factorial(int n) {
if (n <= 1)
return 1;
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
// Add two doubles
double add_doubles(double a, double b) {
return a + b;
}Step 2: Compile into a Shared Library
Compile the C file into a shared library (.so on Linux/macOS, .dll on Windows).
Linux
gcc -fPIC -shared -o libmathutils.so mathutils.c -lmmacOS
gcc -fPIC -shared -o libmathutils.dylib mathutils.c -lmWindows (using MinGW)
gcc -shared -o mathutils.dll mathutils.c| Flag | Purpose |
|---|---|
-fPIC |
Generate position-independent code (required for shared libraries) |
-shared |
Create a shared library instead of an executable |
-o |
Specify the output file name |
-lm |
Link the math library (if using math.h) |
Step 3: Call from Python Using ctypes
The ctypes module is part of Python's standard library: no installation required.
import ctypes
lib = ctypes.CDLL("./libmathutils.so") # Use full path if needed
lib.is_power_of_two.argtypes = [ctypes.c_int]
lib.is_power_of_two.restype = ctypes.c_int
print(f"Is 16 a power of 2? {lib.is_power_of_two(16)}") # 1 (True)
print(f"Is 15 a power of 2? {lib.is_power_of_two(15)}") # 0 (False)
lib.factorial.argtypes = [ctypes.c_int]
lib.factorial.restype = ctypes.c_long
print(f"Factorial of 10: {lib.factorial(10)}") # 3628800
lib.add_doubles.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add_doubles.restype = ctypes.c_double
print(f"3.14 + 2.86 = {lib.add_doubles(3.14, 2.86)}") # 6.0Output:
Is 16 a power of 2? 1
Is 15 a power of 2? 0
Factorial of 10: 3628800
3.14 + 2.86 = 6.0Understanding argtypes and restype
Setting argtypes and restype is essential for correct behavior. Without them, ctypes assumes all arguments and return values are C int types, which causes incorrect results for floats, doubles, pointers, and other types.
lib.add_doubles.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add_doubles.restype = ctypes.c_doubleCommon ctypes Type Mappings
| C Type | ctypes Type | Python Type |
|---|---|---|
int |
ctypes.c_int |
int |
long |
ctypes.c_long |
int |
float |
ctypes.c_float |
float |
double |
ctypes.c_double |
float |
char |
ctypes.c_char |
bytes |
char* |
ctypes.c_char_p |
bytes |
void* |
ctypes.c_void_p |
int or None |
bool |
ctypes.c_bool |
bool |
Working with Strings
Passing strings between Python and C requires using c_char_p for C-style strings:
strutils.c:
int string_length(const char* str) {
return strlen(str);
}Compile:
gcc -fPIC -shared -o libstrutils.so strutils.cPython:
import ctypes
lib = ctypes.CDLL("./libstrutils.so")
lib.string_length.argtypes = [ctypes.c_char_p]
lib.string_length.restype = ctypes.c_int
result = lib.string_length(b"Hello, World!")
print(f"String length: {result}")Output:
String length: 13Working with Arrays
Pass Python arrays to C using ctypes array types:
arrayutils.c:
int sum_array(int* arr, int length) {
int sum = 0;
for (int i = 0; i < length; i++) {
sum += arr[i];
}
return sum;
}Python:
import ctypes
lib = ctypes.CDLL("./libarrayutils.so")
lib.sum_array.argtypes = [ctypes.POINTER(ctypes.c_int), ctypes.c_int]
lib.sum_array.restype = ctypes.c_int
data = [10, 20, 30, 40, 50]
arr_type = ctypes.c_int * len(data)
c_array = arr_type(*data)
result = lib.sum_array(c_array, len(data))
print(f"Sum: {result}")Output:
Sum: 150Using cffi as an Alternative
The cffi library provides a more Pythonic and easier-to-use interface for calling C code:
pip install cffifrom cffi import FFI
ffi = FFI()
ffi.cdef("int is_power_of_two(int num);")
lib = ffi.dlopen("./libmathutils.so")
print(lib.is_power_of_two(16)) # 1
print(lib.is_power_of_two(15)) # 0Complete Working Example
Here's a self-contained example you can copy and run:
demo.c:
int multiply(int a, int b) {
return a * b;
}
double circle_area(double radius) {
return 3.14159265358979 * radius * radius;
}Compile:
gcc -fPIC -shared -o libdemo.so demo.cdemo.py:
import ctypes
lib = ctypes.CDLL("./libdemo.so")
lib.multiply.argtypes = [ctypes.c_int, ctypes.c_int]
lib.multiply.restype = ctypes.c_int
lib.circle_area.argtypes = [ctypes.c_double]
lib.circle_area.restype = ctypes.c_double
print(f"7 × 8 = {lib.multiply(7, 8)}")
print(f"Area of circle (r=5): {lib.circle_area(5.0):.4f}")Output:
7 × 8 = 56
Area of circle (r=5): 78.5398Conclusion
Calling C functions from Python using ctypes is a straightforward process: write the C code, compile it into a shared library, and load it in Python with proper type definitions.
- Always set
argtypesandrestypeto ensure correct data passing between Python and C. - For more complex C interfaces, consider using
cffiwhich offers automatic header parsing and a cleaner API.
This technique lets you combine Python's ease of development with C's raw performance, giving the best of both worlds.