ctypes
ctypes is the standard library's runtime foreign-function interface: it loads compiled shared libraries and calls their functions directly from Python, no compiler required.
ctypes is a standard-library module that lets Python call functions in compiled C libraries — and be called by them — without writing or compiling any C code yourself. You describe the native function's signature using ctypes' own type objects, and ctypes handles converting values across the Python/C boundary at call time. That convenience comes with a cost: ctypes performs almost no safety checking, so a wrong type description is a runtime crash waiting to happen, not a caught exception.
What Is ctypes and Why It Exists
ctypes exists so that ordinary Python code can call into existing native libraries — system libraries, vendor-supplied DLLs, legacy C code — without needing a C compiler, a build system, or a purpose-written extension module. It is part of the standard library, so it's always available.
ctypes is a CPython feature built on top of the platform's dynamic linker and calling conventions; behavior can vary across platforms in ways that are inherent to native code, not bugs in ctypes itself.
Loading Native Libraries
ctypes.CDLL loads a shared library using the standard (usually C) calling convention; on Windows, ctypes.WinDLL loads a library that uses the stdcall convention (common for the Win32 API). ctypes.util.find_library helps locate a library by its base name across platforms.
find_library abstracts away platform-specific library naming; CDLL vs WinDLL matters because mismatching the calling convention corrupts the stack on Windows.
import ctypes
import ctypes.util
# Locate and load the C math library portably.
path = ctypes.util.find_library("m") # e.g. "libm.so.6" on Linux
libm = ctypes.CDLL(path)
# On Windows, calling into the Win32 API uses WinDLL instead:
# user32 = ctypes.WinDLL("user32")Basic Types
ctypes provides Python classes that mirror C's fundamental types; you use these both to describe function signatures and to build values to pass across the boundary.
Passing plain Python int, float, bytes, or str arguments often works because ctypes converts them automatically when a function's argtypes are declared — but relying on implicit conversion without declaring argtypes is fragile and is how most ctypes bugs start.
ctypes type C type Python type on the way back
c_bool _Bool bool
c_char char 1-character bytes
c_int / c_long int / long int
c_double double float
c_char_p char * bytes or None
c_void_p void * int address or None
c_wchar_p wchar_t * str or NoneFunction Signatures: argtypes and restype
A loaded library's functions default to assuming every argument is a C int and returning a C int — almost never correct for real functions. Always set .argtypes (a list of ctypes types) and .restype (a single ctypes type, or None for void) before calling.
Declaring argtypes also gives you a Python-level TypeError for an obviously wrong argument type, which is the closest thing ctypes offers to safety.
import ctypes
libm = ctypes.CDLL(ctypes.util.find_library("m"))
libm.pow.argtypes = [ctypes.c_double, ctypes.c_double]
libm.pow.restype = ctypes.c_double
print(libm.pow(2.0, 10.0)) # 1024.0Pointers and byref
Many C functions take pointers so they can write an output value back through an argument ('out parameters'). ctypes.POINTER(type) describes such an argument, and ctypes.byref(obj) is the efficient way to pass the address of an existing ctypes object without constructing a full pointer object.
import ctypes
# A function that fills an int* with a result, conceptually:
# void get_value(int *out);
count = ctypes.c_int(0)
# lib.get_value(ctypes.byref(count))
# print(count.value)
# .value reads/writes the Python-visible content of a ctypes scalar
count.value = 42
print(count.value) # 42Structures and Arrays
ctypes.Structure lets you describe a C struct's exact field layout, and ctypes array types describe fixed-size native arrays. Field order and types must match the native definition exactly — this is an ABI concern, not just a Python detail.
import ctypes
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_int), ("y", ctypes.c_int)]
p = Point(3, 4)
print(p.x, p.y) # 3 4
IntArray5 = ctypes.c_int * 5
nums = IntArray5(1, 2, 3, 4, 5)
print(list(nums)) # [1, 2, 3, 4, 5]Memory Ownership
ctypes does not track ownership across the Python/C boundary. A ctypes object created in Python owns its own buffer and is freed by Python's garbage collector when unreachable; a pointer returned from native code is not automatically freed by Python, and Python objects passed by reference into native code must stay alive (referenced) for as long as the native side might use them.
A classic trap: passing a temporary bytes/string object's pointer into a function that stores it for later use. Once the temporary is garbage-collected, the native side holds a dangling pointer.
import ctypes
# DANGEROUS pattern: the bytes object may be freed once c_char_p is built
def get_dangling_pointer():
return ctypes.c_char_p(b"temporary").value # .value copies out safely here
# The real danger is storing a raw pointer beyond the lifetime of the
# Python object it points into — always keep a reference alive yourself.
buf = ctypes.create_string_buffer(b"hello")
keep_alive = buf # as long as 'keep_alive' exists, the buffer is safe to useSafety Limitations
ctypes performs essentially no runtime safety checking beyond basic argtypes/restype conversion. A wrong type, an incorrect struct layout, an out-of-bounds array access, or a use-after-free on the native side will not raise a clean Python exception — it can corrupt memory silently or crash the entire process, taking down the interpreter with it.
This is fundamentally different from ordinary Python errors: a bug here is not recoverable with try/except, because the corruption or crash happens below the level Python's exception machinery can see.
Practical Usage Patterns
A typical ctypes binding wraps each native call behind a small Python function that sets up argtypes/restype once (often at import time) and validates or converts inputs before calling, so callers of your wrapper get normal Python errors instead of raw ctypes failures.
Validating in Python before the native call catches errors as ordinary exceptions instead of letting undefined native behavior handle them.
import ctypes
import ctypes.util
_libm = ctypes.CDLL(ctypes.util.find_library("m"))
_libm.sqrt.argtypes = [ctypes.c_double]
_libm.sqrt.restype = ctypes.c_double
def safe_sqrt(x: float) -> float:
if x < 0:
raise ValueError("safe_sqrt requires a non-negative number")
return _libm.sqrt(x)
print(safe_sqrt(9.0)) # 3.0Edge Cases
Passing a str where c_char_p (bytes) is expected fails or misbehaves — encode explicitly. Struct field order must match the platform's alignment/padding rules, not just the field list order in an ambiguous case. errno is not automatically surfaced by ctypes; use ctypes.get_errno() (after enabling use_errno=True on the library) to inspect it, since Python's own errno module reads a different, unrelated copy.
Best Practices
Always set argtypes and restype explicitly. Keep Python references alive for any buffer a native function might retain a pointer to. Wrap raw ctypes calls in a Python API that validates inputs and translates failure conditions into Python exceptions. Prefer a well-tested third-party binding (or cffi) over hand-writing bindings for anything beyond a few functions.
Complete Example
Define a ctypes Structure mirroring a C struct.
Load the native library and declare the function's exact signature.
Wrap the raw call in a typed Python function for a clean call site.
import ctypes
import ctypes.util
class Point(ctypes.Structure):
_fields_ = [("x", ctypes.c_double), ("y", ctypes.c_double)]
libm = ctypes.CDLL(ctypes.util.find_library("m"))
libm.hypot.argtypes = [ctypes.c_double, ctypes.c_double]
libm.hypot.restype = ctypes.c_double
def distance_from_origin(p: Point) -> float:
return libm.hypot(p.x, p.y)
origin_offset = Point(3.0, 4.0)
print(distance_from_origin(origin_offset)) # 5.0Mental Model
Think of ctypes as handing raw bytes across a wall with no inspector on the other side: you must describe exactly what shape those bytes are (argtypes, restype, Structure layout) because nothing will stop you from describing them wrong — the native side will simply read or write memory according to whatever you told it, correct or not.
Quick Reference
CDLL(path) load a library, C calling convention
WinDLL(path) load a library, stdcall (Windows)
fn.argtypes list of ctypes types describing parameters
fn.restype ctypes type describing the return value (or None)
POINTER(t) / byref(x) describe / pass a pointer to x
Structure describe a C struct via _fields_
create_string_buffer mutable bytes buffer for out-parameters
c_int * n fixed-size native array typeKey Takeaways
ctypes is a standard-library way to call native shared libraries at runtime, with no compiler needed.
Always declare argtypes and restype — the defaults (everything is a C int) silently corrupt most real calls.
ctypes does no meaningful safety checking; a wrong type or layout can crash the process instead of raising a Python exception.
Memory ownership across the boundary is your responsibility — keep Python objects alive as long as native code might hold a pointer into them.
lib = ctypes.CDLL("libexample.so") lib.compute(3.5, 2) # no argtypes/restype set lib = ctypes.CDLL("libexample.so") lib.compute.argtypes = [ctypes.c_double, ctypes.c_int] lib.compute.restype = ctypes.c_double result = lib.compute(3.5, 2)
Without argtypes, ctypes truncates the double 3.5 down to a C int when it guesses the type, and the returned value is misread as an int — declare the signature explicitly.
def make_buffer(): s = "hello" return ctypes.c_char_p(s.encode()) # temporary bytes may be freed def make_buffer(): data = b"hello" # keep the bytes object alive alongside the pointer buf = ctypes.c_char_p(data) return buf, data # caller must keep 'data' referenced too
A ctypes pointer to a temporary Python object's buffer is only safe as long as something still references that object; once it's garbage-collected the pointer dangles.
What happens if you call a ctypes function without setting argtypes and restype?
ctypes assumes every argument and the return value are C int, silently truncating or misinterpreting any other type — this is a very common source of ctypes bugs and crashes.
What is the difference between ctypes.CDLL and ctypes.WinDLL?
CDLL loads a library using the standard C calling convention; WinDLL (Windows-only) loads one using the stdcall convention used by much of the Win32 API. Using the wrong one corrupts the call stack on Windows.
Why can a ctypes bug crash the entire Python process instead of raising an exception?
ctypes calls into native code that operates outside Python's memory-safety and exception machinery; a bad pointer or type mismatch causes undefined behavior at the OS/memory level, which Python cannot intercept or recover from.
What does ctypes.byref(x) do, and why prefer it over ctypes.pointer(x)?
byref creates a lightweight, call-only reference to x's address for passing to a function argument; it's cheaper than building a full pointer object with ctypes.pointer(x) when you don't need a standalone pointer value.
Why must a ctypes.Structure's _fields_ match the native struct's layout exactly?
Struct field order, types, and resulting padding/alignment define the memory layout the native code expects; any mismatch causes fields to be read from the wrong offsets, corrupting data silently.
Give one reason to wrap raw ctypes calls in a Python function rather than calling the library object directly everywhere.
A wrapper can validate inputs and raise ordinary Python exceptions for bad values, centralize the argtypes/restype setup, and keep any Python objects that must stay alive properly referenced.
Define a ctypes Structure for a C struct struct Rect { int width; int height; } and write a function that computes its area using ctypes types.
import ctypes class Rect(ctypes.Structure): _fields_ = [("width", ctypes.c_int), ("height", ctypes.c_int)] def area(r: Rect) -> int: return r.width * r.height box = Rect(width=4, height=5) print(area(box)) # 20
The _fields_ list mirrors the C struct's field names, types, and order exactly; ctypes handles allocating the correctly laid-out memory block and exposing fields as normal Python attributes.
argtypes/restype were never set on lib.pow, which actually expects two doubles and returns a double. What is likely to go wrong?
ctypes defaults arguments and the return type to C int, so the integer literals 2 and 10 get passed as ints rather than the doubles pow expects, and the double return value is misread as an int — the printed result is unreliable garbage rather than 1024, and worse mismatches can crash the process.
Explain why keeping a Python reference to a bytes object matters when you pass its address into a native function that stores the pointer for later use.
ctypes does not keep the Python object alive just because native code holds a pointer derived from it; if nothing in Python still references the bytes object, the garbage collector may reclaim it, leaving the native side with a dangling pointer into freed memory — a use-after-free bug.
import ctypes
lib = ctypes.CDLL("libm.so.6")
result = lib.pow(2, 10)
print(result)