Question

Calling C and C++ from Python: Bindings on Windows

cpppythonc

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 ctypes module. 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.

Take Quiz

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:

  1. CDLL("mathlib.dll") asks Windows to load the DLL into the Python process.
  2. mathlib.add finds the exported native function named add.
  3. argtypes records that the function accepts two C int values.
  4. restype records that its result is one C int value.
  5. Calling mathlib.add(7, 5) converts the Python integers to C integers and calls the machine-code function.
  6. The C function computes 12 and returns it.
  7. ctypes converts that C integer to a Python int.
  8. print(answer) displays 12.

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.

Take Quiz

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 as create_engine, engine_process, and destroy_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_* with destroy_* 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

ApproachBest forMain advantageMain limitation
ctypesExisting C DLLs with simple functionsIncluded with Python; no extension compilationManual type declarations; awkward for C++ classes
cffiC libraries, especially when declarations are availableGood C interoperability and ABI/API modesAdds a dependency and still targets C interfaces
CythonPython-like code that calls C/C++ or needs speedConcise syntax and strong Python integrationRequires compiling generated extension code
pybind11C++ libraries and classesNatural C++ bindings and clean Python APIRequires C++ compilation and build configuration
CPython C APILow-level or existing extension codeMaximum control; no extra binding layerVerbose 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.

Take Quiz

Cheat Sheet

  • C DLL with simple functions: start with ctypes.

  • C++ classes or templates: prefer pybind11.

  • Tell ctypes every 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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more