Question
What is the quickest practical way to create Python bindings for an existing C or C++ library? I am developing on Windows; does that affect the recommended approach?
Short Answer
Python can use native C and C++ code through several binding approaches. By the end of this page, you will know when to use ctypes, Cython, pybind11, or a CPython extension; how C and C++ interfaces differ; and how to load a simple Windows DLL from Python.
Concept
Python code normally runs inside the CPython interpreter, while C and C++ libraries are compiled into native machine code. A binding is the layer that lets Python call functions, create objects, and exchange data with that compiled code.
The quickest approach depends mainly on what the library exposes:
- If it has a simple C-compatible DLL API, use Python's standard-library
ctypesmodule. No Python extension needs to be compiled. - If it is a C++ library with classes, templates, or STL types, use pybind11 in most modern projects. It creates a compiled Python extension with a natural Python-facing API.
- If the main work is writing fast code in a Python-like syntax, use Cython.
- If a project already uses the CPython C API, a handwritten extension is possible, but it requires more boilerplate.
C and C++ are important to distinguish. C function names and calling conventions can be exposed in a stable binary interface. C++ names are usually mangled, and C++ classes do not have a universal binary interface across compiler versions and settings. For that reason, a C++ library often exposes a small extern "C" wrapper API, or is bound directly with pybind11.
On Windows, the compiled library must match the Python process architecture: 64-bit Python requires a 64-bit DLL. You must also ensure that the DLL and any DLLs it depends on can be found when Python loads it.
Mental Model
Think of Python and a native library as two people who speak different languages.
A binding is an interpreter at the door:
- Python says, “call
add(2, 3).” - The binding converts Python values into the integer format expected by the native function.
- The C or C++ code runs at native speed.
- The binding converts the result back into a Python integer.
A C API is like a standard form with clearly labelled fields, so ctypes can often use it directly. A C++ class hierarchy is more like a private filing system with compiler-specific conventions, so a tool such as pybind11 is usually needed to present it cleanly to Python.
Syntax and Examples
For a C DLL with simple exported functions, ctypes is often the shortest route.
from ctypes import CDLL, c_int
from pathlib import Path
library_path = Path(__file__).with_name("mathlib.dll")
mathlib = CDLL(str(library_path))
# Describe the native function signature.
mathlib.add.argtypes = [c_int, c_int]
mathlib.add.restype = c_int
result = mathlib.add(20, 22)
print(result) # 42
CDLL loads a library that uses the usual C calling convention. argtypes tells ctypes how to convert the Python arguments, and restype tells it how to convert the return value. Declaring both is important: it documents the interface and prevents incorrect conversions.
For C++ classes, pybind11 lets you write a small C++ wrapper:
#include <pybind11/pybind11.h>
int add(int left, int right) {
return left + right;
}
PYBIND11_MODULE(mathlib, module) {
module.def("add", &add, "Add two integers");
}
After building this as a Python extension module, Python uses it normally:
import mathlib
print(mathlib.add(20, 22)) # 42
The pybind11 wrapper is compiled, but Python callers do not need to know C++ syntax or memory-management details.
Step by Step Execution
Consider this Python code after mathlib.dll has been built:
from ctypes import CDLL, c_int
mathlib = CDLL("mathlib.dll")
mathlib.add.argtypes = [c_int, c_int]
mathlib.add.restype = c_int
answer = mathlib.add(7, 5)
print(answer)
Execution proceeds as follows:
CDLL("mathlib.dll")asks Windows to load the DLL into the Python process.mathlib.addfinds the exported native function namedadd.argtypesrecords that the function accepts two Cintvalues.restyperecords that its result is one Cintvalue.- Calling
mathlib.add(7, 5)converts the Python integers to C integers and calls the machine-code function. - The C function computes
12and returns it. ctypesconverts that C integer to a Pythonint.print(answer)displays12.
Without an accurate signature, ctypes may pass data incorrectly. This is especially dangerous with pointers, floating-point values, structures, and 64-bit values.
Real World Use Cases
Bindings are useful when Python should control an existing native component rather than reimplement it.
- Hardware and vendor SDKs: Call functions supplied by a camera, barcode scanner, instrument, or industrial controller DLL.
- Performance-critical algorithms: Keep an image-processing, simulation, compression, or signal-processing routine in C/C++ and orchestrate it from Python.
- Legacy libraries: Add Python automation to an established C or C++ codebase.
- Operating-system APIs: Access a C-based system library when no suitable Python package exists.
- Custom data formats: Reuse a native parser or encoder from a Python data pipeline.
In each case, the boundary should be small and clear: pass primitive values, buffers, or well-defined structures instead of exposing every internal native type.
Real Codebase Usage
Real projects usually avoid exposing an entire C++ implementation directly. Instead, they design a narrow, stable boundary.
Common patterns include:
- C wrapper around C++: Export
extern "C"functions such ascreate_engine,engine_process, anddestroy_engine. The wrapper hides C++ classes and exceptions from foreign callers. - pybind11 adapter module: Bind selected C++ classes and methods, translate C++ exceptions into Python exceptions, and convert containers to Python lists or dictionaries where appropriate.
- Validation at the boundary: Check Python inputs before entering native code. Validate array shape, length, null pointers, ranges, and configuration values.
- Explicit ownership: Document whether Python owns a returned resource and who is responsible for releasing it. In C APIs, pairing
create_*withdestroy_*is common. - Error-code translation: Convert native error codes into Python exceptions rather than forcing callers to inspect numeric return values.
- Build and packaging automation: Build extension modules with a repeatable tool such as
setuptools,scikit-build-core, or CMake, then test on the same Windows architecture and Python versions users will run.
Keep native interfaces conservative. Plain numbers, UTF-8 strings, byte buffers, and opaque handles are usually easier to maintain than compiler-specific C++ types.
Common Mistakes
Loading a C++ DLL as though it exported ordinary C names
This often fails because C++ compilers mangle function names.
// C++ code: the exported symbol may not be named exactly "add".
int add(int left, int right) {
return left + right;
}
For a C-compatible exported API, use an extern "C" wrapper in C++:
extern "C" __declspec(dllexport)
int add(int left, int right) {
return left + right;
}
Or bind C++ directly with pybind11.
Omitting argtypes and restype
library = CDLL("mathlib.dll")
print(library.add(1, 2)) # Signature is unknown to ctypes.
This may appear to work for simple integers but can fail for pointers, double, structures, or values whose size differs from the default C int. Always declare the signature.
Architecture mismatch
A 32-bit DLL cannot load into a 64-bit Python process, and vice versa. Check Python with:
import struct
print(struct.calcsize("P") * 8)
Build the DLL for the same number of bits.
Passing a Python string where native code expects bytes
C APIs commonly expect char * data. Encode text deliberately:
name = "Ada".encode("utf-8")
Then use a suitable ctypes type such as c_char_p when the C API expects a null-terminated UTF-8 string.
Letting C++ exceptions cross a C boundary
Do not allow an exception to escape through an extern "C" function. Catch it in the wrapper and return an error code or store an error message that Python can retrieve.
Assuming the DLL can always be found
On Windows, use an explicit path during development, and make dependent DLLs available. A native module can fail to load because one of its dependencies is missing, even when the main DLL exists.
Comparisons
| Approach | Best for | Main advantage | Main limitation |
|---|---|---|---|
ctypes | Existing C DLLs with simple functions | Included with Python; no extension compilation | Manual type declarations; awkward for C++ classes |
cffi | C libraries, especially when declarations are available | Good C interoperability and ABI/API modes | Adds a dependency and still targets C interfaces |
| Cython | Python-like code that calls C/C++ or needs speed | Concise syntax and strong Python integration | Requires compiling generated extension code |
| pybind11 | C++ libraries and classes | Natural C++ bindings and clean Python API | Requires C++ compilation and build configuration |
| CPython C API | Low-level or existing extension code | Maximum control; no extra binding layer | Verbose and easier to get wrong |
For a small C DLL, start with ctypes. For a maintained C++ library intended for Python users, pybind11 is generally the clearer long-term choice. For new performance code that is mostly Python logic, Cython may be simpler.
Cheat Sheet
-
C DLL with simple functions: start with
ctypes. -
C++ classes or templates: prefer pybind11.
-
Tell
ctypesevery signature:library.function.argtypes = [c_int, c_int] library.function.restype = c_int -
Load a regular C-convention DLL:
from ctypes import CDLL library = CDLL("path/to/library.dll") -
Windows architecture rule: Python and the DLL must both be 32-bit or both be 64-bit.
-
C++ export rule: use
extern "C"for a C-compatible wrapper, or use pybind11. -
Ownership rule: every native allocation needs a clear release strategy.
-
Error rule: convert native failures into Python exceptions or explicit error results.
-
String rule: know whether the native API expects UTF-8 bytes, UTF-16 wide strings, or another format.
-
Dependency rule: a load failure can mean a dependent DLL is missing, not only that the requested DLL is absent.
FAQ
What is the easiest way to call a C DLL from Python?
For simple exported C functions, use ctypes. It is included in Python and can load a DLL directly.
Can Python call C++ code directly with ctypes?
Not reliably for C++ classes or ordinary C++ symbols. ctypes works best with a C-compatible exported interface. Use pybind11 for direct C++ bindings, or create an extern "C" wrapper.
Is pybind11 only for C++?
Yes. pybind11 is a C++ library for creating Python extension modules. It is particularly useful for exposing C++ functions and classes.
Does Windows change how Python bindings work?
The overall ideas are the same, but Windows uses DLLs and requires attention to DLL search paths, compiler settings, calling conventions, and 32-bit versus 64-bit compatibility.
Why should I set argtypes and restype in ctypes?
They ensure Python converts arguments and return values using the correct native representation. Leaving them unspecified can cause crashes or corrupted values.
Can a binding make C++ code faster?
A binding does not make the native algorithm faster. It lets Python call already-compiled native code, but frequent tiny calls can still be slowed by crossing the Python/native boundary.
How should a C library return errors to Python?
A C API commonly returns an error code and provides an error-message function. The Python wrapper should check the result and raise a meaningful Python exception.
Which compiler should I use on Windows?
Use the compiler and build settings expected by the library and Python-extension build tooling. For a standalone C DLL loaded with ctypes, the key requirement is a compatible exported C ABI and matching architecture.
Mini Project
Description
Build a small Windows C DLL that exposes an add function, then call it safely from Python with ctypes. This demonstrates the simplest practical binding: Python loads a C-compatible library, declares its function signature, and calls native code.
Goal
Compile mathlib.dll and run a Python script that prints the result of a native addition function.
Requirements
Create a C source file that exports an add function from a DLL.
Compile the source into a DLL that matches your Python architecture.
Write a Python script that loads the DLL with ctypes.
Declare the argument and return types before calling the function.
Run the script and verify that it prints 42.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.