Skip to main content

Python C Extensions and FFI Practice Problems & Exercises

Practice: C Extensions and FFI

11 problems3 Easy4 Medium4 Hard60–90 min
← Back to lesson
#1Load a Shared Library with ctypesEasy
ctypesffishared-library

Use ctypes to load the C math library (libm) and call acos(-1.0) to obtain the value of pi.

Expected output

3.141592653589793

This tests that you can locate, load, and call a function from a real system shared library.

Solution
import ctypes
import ctypes.util

def get_pi_from_libm() -> float:
lib_name = ctypes.util.find_library("m")
libm = ctypes.CDLL(lib_name)
libm.acos.argtypes = [ctypes.c_double]
libm.acos.restype = ctypes.c_double
return libm.acos(-1.0)

print(get_pi_from_libm()) # 3.141592653589793

Key steps: find the library name portably with find_library, load with CDLL, declare argument and return types to prevent silent mismatches, then call like a Python function.

import ctypes
import ctypes.util

def get_pi_from_libm() -> float:
  """Load libm and call acos(-1.0) to retrieve pi."""
  # TODO: load libm, set up acos signature, call it
  pass
Expected Output
3.141592653589793
Hints

Hint 1: Use ctypes.CDLL to load libm (the C math library).

Hint 2: On Linux/macOS the name is "libm.so.6" or "libm.dylib"; use ctypes.util.find_library("m") to be portable.

Hint 3: Set argtypes and restype before calling.


#2ctypes Basic Types and Return ValuesEasy
ctypesbasic-typeslibc

Implement c_abs(value) by calling the C standard library's abs() function via ctypes.

Example

c_abs(-42) # → 42
c_abs(7) # → 7
Solution
import ctypes
import ctypes.util

def c_abs(value: int) -> int:
libc = ctypes.CDLL(ctypes.util.find_library("c"))
libc.abs.argtypes = [ctypes.c_int]
libc.abs.restype = ctypes.c_int
return libc.abs(value)

ctypes.c_int maps to int (32-bit signed). Without explicit argtypes / restype, ctypes defaults both to c_int, which silently truncates 64-bit values or misinterprets doubles.

import ctypes
import ctypes.util

def c_abs(value: int) -> int:
  """Call C's abs() via ctypes and return the result."""
  pass
Expected Output
absolute value: 42
Hints

Hint 1: Load libc (find_library("c")).

Hint 2: abs() takes c_int and returns c_int.

Hint 3: Always set argtypes and restype — without them ctypes assumes c_int for everything.


#3Pass a Python String to CEasy
ctypesstringsc_char_p

Implement c_strlen(s) that uses ctypes to call the C strlen() function on a Python string and return the result as a Python int.

Example

c_strlen("hello") # → 5
c_strlen("ctypes") # → 6
Solution
import ctypes
import ctypes.util

def c_strlen(s: str) -> int:
libc = ctypes.CDLL(ctypes.util.find_library("c"))
libc.strlen.argtypes = [ctypes.c_char_p]
libc.strlen.restype = ctypes.c_size_t
return libc.strlen(s.encode())

c_char_p accepts bytes objects and passes the underlying buffer pointer. Always .encode() Python strings — C expects bytes, not Unicode objects. c_size_t maps to the platform's size_t (64-bit on modern systems).

import ctypes
import ctypes.util

def c_strlen(s: str) -> int:
  """Call C's strlen() to get the byte length of a Python string."""
  pass
Expected Output
length of 'hello' is 5
Hints

Hint 1: C expects a null-terminated byte string: encode with b"hello" or str.encode().

Hint 2: Use ctypes.c_char_p for char* parameters.

Hint 3: strlen returns size_t — use ctypes.c_size_t as restype.


#4ctypes Structure — Interop with C StructsMedium
ctypesStructurestruct-interop

Define a ctypes.Structure for a 2-D point with double coordinates, then compute its distance from the origin in Python.

Example

p = Point(x=3.0, y=4.0)
distance_from_origin(p) # → 5.0

This mirrors how you would share a struct definition between Python and a C library that expects struct Point { double x; double y; }.

Solution
import ctypes
import math

class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]

def distance_from_origin(p: Point) -> float:
return math.hypot(p.x, p.y)

p = Point(x=3.0, y=4.0)
print(distance_from_origin(p)) # 5.0

_fields_ defines the memory layout. ctypes auto-generates __init__ accepting keyword arguments that match field names. The struct is memory-compatible with struct Point { double x; double y; } in C (no padding on most platforms for two consecutive doubles).

import ctypes

class Point(ctypes.Structure):
  _fields_ = [
      # TODO: define x and y as c_double
  ]

def distance_from_origin(p: Point) -> float:
  """Return Euclidean distance of point p from the origin."""
  pass
Expected Output
Point(x=3.0, y=4.0), distance=5.0
Hints

Hint 1: Subclass ctypes.Structure and define _fields_ as a list of (name, ctype) tuples.

Hint 2: Pass a pointer to the struct via ctypes.byref().

Hint 3: Accessing .x and .y on the instance returns Python floats automatically.


#5Callback Function — Passing Python Code to CMedium
ctypescallbackCFUNCTYPE

Implement sort_with_c_qsort that:

  1. Creates a ctypes array from the input list.
  2. Defines a Python comparator function using CFUNCTYPE.
  3. Passes that comparator to libc's qsort.
  4. Returns the sorted list.

Example

sort_with_c_qsort([5, 2, 8, 1, 9, 3])
# → [1, 2, 3, 5, 8, 9]
Solution
import ctypes
import ctypes.util

def sort_with_c_qsort(values: list) -> list:
libc = ctypes.CDLL(ctypes.util.find_library("c"))

n = len(values)
arr = (ctypes.c_int * n)(*values)

CMP = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p)

def comparator(a_ptr, b_ptr):
a = ctypes.cast(a_ptr, ctypes.POINTER(ctypes.c_int)).contents.value
b = ctypes.cast(b_ptr, ctypes.POINTER(ctypes.c_int)).contents.value
return (a > b) - (a < b)

cmp_func = CMP(comparator)

libc.qsort.argtypes = [
ctypes.c_void_p,
ctypes.c_size_t,
ctypes.c_size_t,
CMP,
]
libc.qsort.restype = None
libc.qsort(arr, n, ctypes.sizeof(ctypes.c_int), cmp_func)

return list(arr)

CFUNCTYPE wraps the Python function in a C-callable trampoline. The GC must keep cmp_func alive for the duration of qsort — store it in a variable, not a temporary.

import ctypes
import ctypes.util

def sort_with_c_qsort(values: list) -> list:
  """
  Sort a list of ints using C's qsort() with a Python comparator callback.
  Return the sorted list.
  """
  pass
Expected Output
comparator called by qsort, array sorted correctly
Hints

Hint 1: Define the callback type with ctypes.CFUNCTYPE(return_type, *arg_types).

Hint 2: The qsort comparator signature is int cmp(const void*, const void*).

Hint 3: Cast the void pointers to ctypes.POINTER(ctypes.c_int) to dereference them.


#6Read and Write a C Array via PointerMedium
ctypespointerarraymemory

Implement double_array_inplace that allocates a C integer array, walks it via a raw ctypes pointer (incrementing each element by multiplying by 2), and returns the result as a Python list.

Example

double_array_inplace([1, 2, 3, 4, 5])
# → [2, 4, 6, 8, 10]
Solution
import ctypes

def double_array_inplace(data: list) -> list:
n = len(data)
arr_type = ctypes.c_int * n
arr = arr_type(*data)

ptr = ctypes.cast(arr, ctypes.POINTER(ctypes.c_int))
for i in range(n):
ptr[i] *= 2

return list(arr)

ctypes.cast reinterprets the array buffer as a pointer. Subscript access ptr[i] performs the pointer offset arithmetic — equivalent to *(ptr + i) in C. The underlying buffer is shared, so arr reflects the changes immediately.

import ctypes

def double_array_inplace(data: list) -> list:
  """
  Create a ctypes c_int array from data, double every element
  via pointer arithmetic, and return the modified list.
  """
  pass
Expected Output
doubled array: [2, 4, 6, 8, 10]
Hints

Hint 1: Create a ctypes array type: (c_int * n)(*data).

Hint 2: Pass it to ctypes.cast(..., POINTER(c_int)) to get a raw pointer.

Hint 3: Index the pointer with [i] to read or write individual elements.


#7cffi In-line Mode — Declare and CallMedium
cffiffiinline-mode

Use cffi (in API/in-line mode) to call floor() from libm.

Example

floor_via_cffi(3.7) # → 3.0
floor_via_cffi(-1.2) # → -2.0
Solution
import cffi
import ctypes.util

def floor_via_cffi(value: float) -> float:
ffi = cffi.FFI()
ffi.cdef("double floor(double x);")
lib_path = ctypes.util.find_library("m")
libm = ffi.dlopen(lib_path)
return libm.floor(value)

ffi.cdef() parses a subset of C declarations. ffi.dlopen() loads the shared library. cffi automatically marshals Python floats to C double and back, and its overhead per call is lower than ctypes for hot paths because the generated trampoline is more direct.

def floor_via_cffi(value: float) -> float:
  """Use cffi to call libm's floor() function."""
  # TODO: import cffi, declare floor, call it
  pass
Expected Output
floor(3.7) = 3.0
Hints

Hint 1: Use cffi.FFI() and call ffi.cdef() with the C declaration.

Hint 2: Open the library with ffi.dlopen(lib_path).

Hint 3: Call the function through the returned lib object.


#8Write a Minimal C Extension ModuleHard
C-extensioncpython-apiPyArg_ParseTuple

Write the complete C source for a CPython extension module named myext that exposes a single function add(a: int, b: int) -> int.

Your answer should include:

  1. The full .c source with correct PyMethodDef, PyModuleDef, and PyMODINIT_FUNC.
  2. A brief explanation of each section.

(You do not need to actually compile it — focus on correctness of the source.)

Solution
/* myext.c */
#define PY_SSIZE_T_CLEAN
#include <Python.h>

/* The actual function */
static PyObject *
myext_add(PyObject *self, PyObject *args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b))
return NULL; /* returns NULL on parse error */
return PyLong_FromLong(a + b); /* new reference — Python owns it */
}

/* Method table */
static PyMethodDef MyextMethods[] = {
{"add", myext_add, METH_VARARGS, "Add two integers."},
{NULL, NULL, 0, NULL} /* sentinel */
};

/* Module definition */
static struct PyModuleDef myextmodule = {
PyModuleDef_HEAD_INIT,
"myext", /* module name */
NULL, /* module docstring (optional) */
-1, /* per-interpreter state size; -1 = global */
MyextMethods
};

/* Init function — called when Python imports the module */
PyMODINIT_FUNC
PyInit_myext(void)
{
return PyModule_Create(&myextmodule);
}

Section breakdown:

  • PyArg_ParseTuple: parses positional args from Python into C variables. "ll" means two C long values.
  • PyLong_FromLong: converts a C long back to a Python int object, incrementing the reference count.
  • PyMethodDef: maps Python name → C function pointer + calling convention + docstring.
  • PyModuleDef + PyModule_Create: registers the module with the interpreter.
  • PyMODINIT_FUNC PyInit_myext: the mandatory entry point — name must match the module name.
# myext.c — write the C source as a Python string,
# then compile and import it at runtime using cffi or distutils.
# For this exercise, write out the complete C source that would
# implement a module named "myext" with a single function add(a, b).

C_SOURCE = """
/* TODO: fill in the CPython extension C source */
"""
Expected Output
myext.add(3, 4) == 7
Hints

Hint 1: Define a C function with signature PyObject* add(PyObject* self, PyObject* args).

Hint 2: Use PyArg_ParseTuple(args, "ll", &a, &b) to extract two C longs.

Hint 3: Return PyLong_FromLong(a + b).

Hint 4: Register in a PyMethodDef array and create a module with PyModule_Create.


#9Buffer Protocol — Zero-copy Memory AccessHard
ctypesbuffer-protocolmemoryviewzero-copy

Implement zero_copy_sum that accesses an array.array of doubles through the buffer protocol and sums the values via ctypes — without copying the buffer.

Example

import array
data = array.array("d", [1.1, 2.2, 3.3, 4.4])
zero_copy_sum(data) # → 11.0
Solution
import ctypes
import array

def zero_copy_sum(arr: array.array) -> float:
n = len(arr)
# from_buffer shares the memory — no copy
c_arr = (ctypes.c_double * n).from_buffer(arr)
total = 0.0
for v in c_arr:
total += v
return total

from_buffer creates a ctypes array that aliases the same memory as arr. Writing to c_arr[i] would also modify arr[i]. This is the foundation of how NumPy, PIL, and other libraries expose their internal buffers to C extensions without serialisation.

Production use: prefer sum(c_arr) or pass the pointer to a C BLAS routine for real workloads.

import ctypes
import array

def zero_copy_sum(arr: array.array) -> float:
  """
  Compute the sum of a Python array.array of doubles
  using ctypes, without copying the data.
  Return the result as a Python float.
  """
  pass
Expected Output
sum computed without copying array data
Hints

Hint 1: Use memoryview(arr) to get a view of the underlying buffer.

Hint 2: Pass memoryview to ctypes via (c_double * n).from_buffer(mv).

Hint 3: from_buffer shares memory — no copy occurs.


#10Wrap a C Function That Mutates Its ArgumentHard
ctypesbyrefoutput-parameterpointer

Model the common C pattern where a function writes its result into a pointer argument (double *output). Use ctypes.byref (or ctypes.POINTER) to receive the out-parameter.

Since we cannot compile C inline here, implement using math.sqrt as the underlying engine but wrap the call in the full ctypes out-parameter dance to demonstrate the pattern.

Example

call_compute_sqrt(9.0) # → 3.0
call_compute_sqrt(2.0) # → 1.4142...
call_compute_sqrt(-1.0) # → raises ValueError
Solution
import ctypes
import math

# Simulate the C function using a Python ctypes callback
COMPUTE_SQRT_TYPE = ctypes.CFUNCTYPE(
ctypes.c_int,
ctypes.c_double,
ctypes.POINTER(ctypes.c_double),
)

def _impl(input_val, output_ptr):
if input_val < 0:
return -1
output_ptr[0] = math.sqrt(input_val)
return 0

_c_compute_sqrt = COMPUTE_SQRT_TYPE(_impl)

def call_compute_sqrt(value: float) -> float:
result = ctypes.c_double(0.0)
rc = _c_compute_sqrt(value, ctypes.byref(result))
if rc != 0:
raise ValueError(f"compute_sqrt failed for input {value}")
return result.value

ctypes.byref(result) is equivalent to &result in C — it passes the address of the c_double variable. The C function writes through the pointer with *output = ..., which here becomes output_ptr[0] = ... in the Python callback. After the call result.value holds the written value.

import ctypes
import ctypes.util

# Imagine a C function with this signature:
#   int compute_sqrt(double input, double *output);
# It writes the square root into *output and returns 0 on success, -1 on error.
# We simulate it with a small inline C string compiled via cffi.

FAKE_C_SRC = """
int compute_sqrt(double input, double *output) {
  if (input < 0) return -1;
  *output = /* square root calculation */;
  return 0;
}
"""

def call_compute_sqrt(value: float) -> float:
  """
  Call the simulated C function using ctypes byref pattern.
  Return the computed square root, or raise ValueError on negative input.
  """
  pass
Expected Output
result written into output parameter, returned to Python
Hints

Hint 1: Declare the output parameter as POINTER(c_double).

Hint 2: Pass ctypes.byref(result_var) to let C write into result_var.

Hint 3: Read back result_var.value after the call.


#11cffi Out-of-line Mode — Build and Call a Real C FunctionHard
cffiout-of-linecompilationabi-check

Design the complete cffi out-of-line build script for a dot_product(a, b, n) C function, and show how you would:

  1. Write the C implementation inside set_source.
  2. Compile it into a Python extension module.
  3. Call it from Python and verify against numpy.dot.
Solution

Build script (_dot_product_build.py):

import cffi

ffi = cffi.FFI()

ffi.cdef("""
double dot_product(const double *a, const double *b, int n);
""")

ffi.set_source(
"_dot_product",
"""
double dot_product(const double *a, const double *b, int n) {
double result = 0.0;
for (int i = 0; i < n; i++) {
result += a[i] * b[i];
}
return result;
}
""",
)

if __name__ == "__main__":
ffi.compile(verbose=True)

Run once:

python _dot_product_build.py

This produces _dot_product.cpython-3XX-*.so (or .pyd on Windows).

Usage script:

import numpy as np
from _dot_product import ffi, lib

def call_dot(a_list, b_list):
n = len(a_list)
a_buf = ffi.new("double[]", a_list)
b_buf = ffi.new("double[]", b_list)
return lib.dot_product(a_buf, b_buf, n)

a = [1.0, 2.0, 3.0]
b = [4.0, 5.0, 6.0]

result = call_dot(a, b)
expected = np.dot(a, b)
print(f"cffi: {result}") # 32.0
print(f"numpy: {expected}") # 32.0
assert abs(result - expected) < 1e-10

Key differences from ctypes:

  • cffi verifies the declaration against the actual C headers at compile time (ABI-safe).
  • ffi.new("double[]", data) allocates a C array and copies from Python — comparable to (c_double * n)(*data).
  • Out-of-line mode compiles once; imports are as fast as any C extension thereafter.
# Step 1: write the build script content as a Python string.
# Step 2: describe how to run it and import the result.
# Step 3: call dot_product from the compiled module.

BUILD_SCRIPT = """
# _dot_product_build.py — run this once to compile the extension
import cffi
ffi = cffi.FFI()

ffi.cdef("""
  double dot_product(const double *a, const double *b, int n);
""")

ffi.set_source(
  "_dot_product",  # module name
  """
  /* TODO: implement dot_product in C */
  """,
)

if __name__ == "__main__":
  ffi.compile(verbose=True)
"""

def python_dot_product(a: list, b: list) -> float:
  """Pure-Python reference implementation for verification."""
  return sum(x * y for x, y in zip(a, b))
Expected Output
Python calls compiled C dot_product, result matches numpy
Hints

Hint 1: cffi out-of-line mode: write a build script that calls ffi.set_source() then ffi.compile().

Hint 2: In the build script: ffi.cdef() declares the public API; ffi.set_source() provides the C implementation.

Hint 3: Import the generated extension module just like any other Python module.

Hint 4: Verify the result against numpy.dot for correctness.

© 2026 EngineersOfAI. All rights reserved.