How to Call a C Function in Python
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:
#include <math.h>
// 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 -lm
macOS
gcc -fPIC -shared -o libmathutils.dylib mathutils.c -lm
Windows (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
# Load the shared library
lib = ctypes.CDLL("./libmathutils.so") # Use full path if needed
# --- Call is_power_of_two ---
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)
# --- Call factorial ---
lib.factorial.argtypes = [ctypes.c_int]
lib.factorial.restype = ctypes.c_long
print(f"Factorial of 10: {lib.factorial(10)}") # 3628800
# --- Call add_doubles ---
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.0
Output:
Is 16 a power of 2? 1
Is 15 a power of 2? 0
Factorial of 10: 3628800
3.14 + 2.86 = 6.0
Understanding 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.
# Define argument types (what the function expects)
lib.add_doubles.argtypes = [ctypes.c_double, ctypes.c_double]
# Define return type (what the function returns)
lib.add_doubles.restype = ctypes.c_double
argtypes and restypeWithout proper type definitions, you'll get wrong results (not errors) because ctypes silently misinterprets the data:
# Without setting types: WRONG result!
lib.add_doubles.argtypes = None
lib.add_doubles.restype = None
result = lib.add_doubles(3.14, 2.86)
print(result) # Some garbage integer value, not 6.0
# With proper types: CORRECT result
lib.add_doubles.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add_doubles.restype = ctypes.c_double
result = lib.add_doubles(3.14, 2.86)
print(result) # 6.0
Common 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:
#include <string.h>
int string_length(const char* str) {
return strlen(str);
}
Compile:
gcc -fPIC -shared -o libstrutils.so strutils.c
Python:
import ctypes
lib = ctypes.CDLL("./libstrutils.so")
lib.string_length.argtypes = [ctypes.c_char_p]
lib.string_length.restype = ctypes.c_int
# Strings must be encoded to bytes
result = lib.string_length(b"Hello, World!")
print(f"String length: {result}")
Output:
String length: 13
Python strings must be converted to bytes (using b"..." or .encode()) before passing to C functions that expect char*.
Working 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
# Create a C-compatible array from a Python list
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: 150
Using cffi as an Alternative
The cffi library provides a more Pythonic and easier-to-use interface for calling C code:
pip install cffi
from cffi import FFI
ffi = FFI()
# Declare the C function signature
ffi.cdef("int is_power_of_two(int num);")
# Load the shared library
lib = ffi.dlopen("./libmathutils.so")
# Call the function: no type declarations needed!
print(lib.is_power_of_two(16)) # 1
print(lib.is_power_of_two(15)) # 0
ctypes vs cffi| Feature | ctypes | cffi |
|---|---|---|
| Built-in | ✅ (standard library) | ❌ (requires install) |
| Ease of use | Moderate | Easier |
| C header parsing | ❌ Manual | ✅ Automatic via cdef |
| Performance | Good | Slightly better |
| Recommended for | Simple cases | Complex C interfaces |
Complete 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.c
demo.py:
import ctypes
# Load the library
lib = ctypes.CDLL("./libdemo.so")
# Configure multiply
lib.multiply.argtypes = [ctypes.c_int, ctypes.c_int]
lib.multiply.restype = ctypes.c_int
# Configure circle_area
lib.circle_area.argtypes = [ctypes.c_double]
lib.circle_area.restype = ctypes.c_double
# Use the functions
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.5398
Conclusion
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.