C++ Extensions Advanced¶
When you'd use this
Extend Python with C++ using pybind11 for native speed.
Extend Python with C++ for speed or to bind existing native libraries.
What you'll learn¶
- Why write extensions in C++
- pybind11 — the modern binding tool
- Exposing functions and classes
- Managing memory across the boundary
- Build and packaging
When pure Python (even vectorized) isn't fast enough, you can write the hot part in C++ and call it from Python. pybind11 makes this remarkably clean. This complements the Rust path (Rust Extensions).
This topic requires a C++ toolchain
Building C++ extensions needs a compiler and pybind11, not present in this environment, so the code follows pybind11's documented API rather than being run-verified. The concepts and structure are what matter here.
Why C++ extensions¶
A core question explored in C++ Extensions: Why C++ extensions.
Reasons to drop to C++:
- Speed — a tight numeric/algorithmic loop in C++ can be 10-100× faster than pure Python.
- Existing C++ libraries — wrap a mature C++ library to use it from Python.
- Hardware/low-level access — things Python can't do directly.
The pattern: Python for the 95% (glue, I/O, orchestration), C++ for the 5% hot path. You keep Python's productivity and get native speed where it counts.
pybind11: the modern way¶
Bind C++ to Python with a clean header-only library.
Historically, extensions used the raw CPython C API (verbose, error-prone) or SWIG. pybind11 is a header-only C++ library that makes binding concise and safe. A function:
// example.cpp
#include <pybind11/pybind11.h>
int add(int a, int b) {
return a + b;
}
PYBIND11_MODULE(example, m) {
m.doc() = "example module";
m.def("add", &add, "Add two integers");
}
Then from Python:
That PYBIND11_MODULE macro and m.def are all it takes to expose a C++ function to Python — far less boilerplate than the raw C API.
Exposing a C++ class¶
Make a C++ class usable as a Python class.
pybind11 maps C++ classes to Python classes naturally:
#include <pybind11/pybind11.h>
class Accumulator {
public:
Accumulator() : total_(0) {}
void add(int x) { total_ += x; }
int total() const { return total_; }
private:
int total_;
};
PYBIND11_MODULE(example, m) {
pybind11::class_<Accumulator>(m, "Accumulator")
.def(pybind11::init<>())
.def("add", &Accumulator::add)
.def("total", &Accumulator::total);
}
from example import Accumulator
acc = Accumulator()
acc.add(3); acc.add(4)
print(acc.total()) # -> 7
The C++ object behaves like a normal Python object. pybind11 also handles STL containers (std::vector ↔ list), and integrates with NumPy arrays for zero-copy numeric work.
Memory & lifetime across the boundary¶
Manage ownership so objects aren't freed too early or leaked.
The trickiest part is object lifetime — who owns what:
- pybind11 manages Python's reference counting for you in common cases.
- Return-value policies control whether Python takes ownership of a returned C++ object, references it, or copies it — get this wrong and you get crashes or leaks.
- Passing large data (arrays) should be zero-copy where possible (pybind11 + NumPy buffer protocol) to avoid expensive copies across the boundary.
The boundary is where bugs live
Most C++-extension bugs are at the Python↔C++ boundary: ownership mistakes cause use-after-free crashes or leaks, and every crossing has a cost. Design a coarse interface — pass a big chunk of work across once, not millions of tiny calls. A chatty boundary erases the speed you gained.
Build & packaging¶
Compile and ship the extension as an installable wheel.
You compile the C++ into a shared library Python can import:
- setuptools + pybind11 — declare the extension in
pyproject.toml/setup.py;pip installcompiles it. - CMake + pybind11 — for larger C++ projects.
- scikit-build-core — modern bridge between CMake and Python packaging.
The built module is a .so (Linux/macOS) or .pyd (Windows) that imports like any Python module. Distributing means building wheels per platform (see Packaging).
C++ vs Rust vs Cython vs Numba¶
Pick the right acceleration tool for the job.
| Approach | Best when |
|---|---|
| pybind11 (C++) | You know C++ or need existing C++ libraries |
| Rust (Rust Extensions) | Want memory safety + speed; newer projects |
| Cython | Gradually typing Python for speed, less C++ needed |
| Numba | Numeric loops you can JIT with a decorator |
For a new speed-critical extension without an existing C++ codebase, many teams now reach for Rust (PyO3) for its safety. C++ (pybind11) shines when you already have C++ or need its ecosystem.
Practice exercises¶
- Explain when you'd write a C++ extension versus just vectorizing with NumPy.
- Describe why a "chatty" fine-grained boundary can erase the speedup of native code.
- Sketch the pybind11 binding for a function that takes and returns a list of numbers.
- Explain what a return-value policy controls and why it matters for memory safety.
- Compare pybind11 and PyO3 (Rust) for a new extension — what would drive your choice?
💬 Discussion
Have a question about this topic? Found an error? Share your thoughts below.