Skip to main content

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
FlagPurpose
-fPICGenerate position-independent code (required for shared libraries)
-sharedCreate a shared library instead of an executable
-oSpecify the output file name
-lmLink 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
Always set argtypes and restype

Without 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 Typectypes TypePython Type
intctypes.c_intint
longctypes.c_longint
floatctypes.c_floatfloat
doublectypes.c_doublefloat
charctypes.c_charbytes
char*ctypes.c_char_pbytes
void*ctypes.c_void_pint or None
boolctypes.c_boolbool

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
note

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
Featurectypescffi
Built-in✅ (standard library)❌ (requires install)
Ease of useModerateEasier
C header parsing❌ Manual✅ Automatic via cdef
PerformanceGoodSlightly better
Recommended forSimple casesComplex 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 argtypes and restype to ensure correct data passing between Python and C.
  • For more complex C interfaces, consider using cffi which 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.