C Extensions and ctypes

Python 3.14 · ✓ verified by execution on 2026-07-31

Python is a highly dynamic, interpreted language. While it is incredibly expressive, certain computational workloads (like matrix multiplication or cryptographic hashing) simply execute too slowly in pure Python.

To bridge this gap, Python was designed from day one to act as a “glue language” that can seamlessly delegate heavy workloads to compiled C code.

There are three primary ways to achieve this: Foreign Function Interfaces (FFI) like ctypes and cffi, and the native Python C-API.

The ctypes Module

ctypes is a standard library module that allows you to load dynamic libraries (.dll on Windows, .so on Linux, .dylib on macOS) at runtime, construct C-compatible data types in memory, and invoke native functions.

python · visualize
import ctypes

# 1. Load the OS native C standard library
try:
    # Linux/Mac
    libc = ctypes.CDLL("libc.so.6")
except OSError:
    # Windows
    libc = ctypes.cdll.msvcrt

# 2. Call the native C absolute value function
result = libc.abs(-42)
print(f"C says absolute value is: {result}")

# 3. Call native C strlen!
# Note: C expects raw byte arrays, not Python Unicode strings!
length = libc.strlen(b"Hello C!")
print(f"C says string length is: {length}")

Notice that strlen requires a byte string (b"Hello C!"). Because Python Unicode strings are complex objects with headers and variable encodings, passing them directly to C would result in a segfault.

Memory Layout and ctypes.Structure

If a C library requires you to pass a struct, you can mirror that exact memory layout in Python by subclassing ctypes.Structure.

import ctypes

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

# This allocates a continuous block of 16 bytes of memory (2 x 8-byte doubles)
p = Point(10.0, 20.0)
print(p.x, p.y)

The Hidden Cost: Boundary Overhead

A massive misconception is that “putting it in C makes it faster”. If you use ctypes to call a very simple C function inside a tight loop, your code will often run slower than pure Python!

Every time you call libc.abs(-1), Python must:

  1. Box the Python int into a ctypes.c_int.
  2. Push it to the native C call stack.
  3. Perform a dynamic native jump (FFI).
  4. Take the C return value.
  5. Unbox it back into a new Python int object.

The only time C Extensions yield performance gains is when the C function is given a large chunk of data and computes heavily for thousands of CPU cycles before returning across the FFI boundary.

Check yourself

What is the primary function of the `ctypes` module in standard Python?

Reveal answer

To load dynamic C libraries (DLLs/Shared Objects) and call their exported functions directly from pure Python. — `ctypes` is a Foreign Function Interface (FFI) that loads `.dll` or `.so` files at runtime, allowing you to invoke native C code without writing any C glue code yourself.

When calling a C function via `ctypes` inside a tight `for` loop, what is a likely performance outcome compared to pure Python?

Reveal answer

It might actually be slower due to the overhead of boxing and unboxing Python objects into C data types on every single iteration. — FFI boundaries are expensive. If the C function only does a tiny amount of work (like `abs()`), the time spent converting Python integers to C ints and back again vastly outweighs the execution time of the function itself.

If you want to write a true C Extension for Python that manipulates Python lists directly in C, what must you `#include`?

Reveal answer

`<Python.h>` — To interact with Python's internal memory manager, reference counting, and objects from C, you must include `<Python.h>` and write code against the Python C-API.

Challenges

Challenge 1 +100 XP

Use `ctypes` to load the system C library. Then call the native C `strlen()` function on the byte string `b'Hello C!'`. Print the result. (Hint: `libc.strlen` expects a bytes object).

python
  • Test 1 — expects "8\n"
Show solution (0 XP)
import ctypes

try:
    libc = ctypes.CDLL('libc.so.6')
except OSError:
    libc = ctypes.cdll.msvcrt

print(libc.strlen(b'Hello C!'))

Challenge 2 +100 XP

Define a `ctypes.Structure` named `Vector` with two fields: `x` and `y` (both `ctypes.c_double`). Create an instance `v = Vector(3.0, 4.0)` and print `v.x * v.y`.

python
  • Test 1 — expects "12.0\n"
Show solution (0 XP)
import ctypes

class Vector(ctypes.Structure):
    _fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double)]

v = Vector(3.0, 4.0)
print(v.x * v.y)