Python pybind11 C++ Bindings — Core Concepts
Pybind11 is a lightweight, header-only C++ library that exposes C++ types and functions to Python. It leverages C++11 features (variadic templates, move semantics, type inference) to generate bindings with minimal code and fast compilation.
Why pybind11 Exists
Before pybind11, the main options were Boost.Python (heavy dependency, slow compilation) and SWIG (separate code generation step, limited C++ support). Pybind11 was created in 2015 as a modern alternative that keeps Boost.Python’s ergonomic API while dropping the Boost dependency entirely.
Basic Binding Example
#include <pybind11/pybind11.h>
namespace py = pybind11;
int add(int a, int b) { return a + b; }
PYBIND11_MODULE(example, m) {
m.doc() = "Example module";
m.def("add", &add, "Add two integers",
py::arg("a"), py::arg("b"));
}
Python side:
import example
print(example.add(a=3, b=4)) # 7
The py::arg annotations create keyword arguments in Python. Default values work too: py::arg("b") = 0.
Class Bindings
#include <pybind11/pybind11.h>
namespace py = pybind11;
class Pet {
public:
Pet(const std::string& name, int age) : name(name), age(age) {}
std::string name;
int age;
std::string bark() const { return name + " says woof!"; }
};
PYBIND11_MODULE(pets, m) {
py::class_<Pet>(m, "Pet")
.def(py::init<std::string, int>())
.def("bark", &Pet::bark)
.def_readwrite("name", &Pet::name)
.def_readonly("age", &Pet::age)
.def("__repr__", [](const Pet& p) {
return "<Pet '" + p.name + "' age=" + std::to_string(p.age) + ">";
});
}
This creates a fully functional Python class with constructor, methods, readable/writable properties, and a string representation.
NumPy Integration
Pybind11 has first-class NumPy support through pybind11/numpy.h:
#include <pybind11/numpy.h>
py::array_t<double> multiply(py::array_t<double> input, double factor) {
auto buf = input.request(); // Buffer info
double* ptr = static_cast<double*>(buf.ptr);
auto result = py::array_t<double>(buf.size);
auto res_buf = result.request();
double* res_ptr = static_cast<double*>(res_buf.ptr);
for (ssize_t i = 0; i < buf.size; i++)
res_ptr[i] = ptr[i] * factor;
return result;
}
This accepts NumPy arrays directly without copying, processes them in C++, and returns a new NumPy array. The buffer protocol handles memory layout transparently.
Smart Pointer Support
Pybind11 works with std::shared_ptr and std::unique_ptr out of the box:
py::class_<Widget, std::shared_ptr<Widget>>(m, "Widget")
.def(py::init<>());
m.def("create_widget", []() {
return std::make_shared<Widget>();
});
Reference counting is shared between Python and C++. When the last Python reference and the last C++ shared_ptr are both gone, the object is destroyed.
Key Advantages Over Alternatives
| Feature | pybind11 |
|---|---|
| No dependencies | Header-only; just include the headers |
| Fast compilation | Lighter templates than Boost.Python |
| Modern C++ | Uses C++11/14/17 features naturally |
| NumPy support | Built-in, zero-copy array access |
| STL support | Automatic conversion for vector, map, set, optional, variant |
| Lambda support | Bind lambdas directly as Python functions |
Build Options
Using CMake (Recommended)
find_package(pybind11 REQUIRED)
pybind11_add_module(example example.cpp)
Using pip (scikit-build or setuptools)
# pyproject.toml
[build-system]
requires = ["setuptools", "pybind11"]
build-backend = "setuptools.build_meta"
Using pybind11’s own build helper
from pybind11.setup_helpers import Pybind11Extension, build_ext
from setuptools import setup
setup(
ext_modules=[Pybind11Extension("example", ["example.cpp"])],
cmdclass={"build_ext": build_ext},
)
Common Misconception
“pybind11 is slow because it’s header-only.” Header-only means no library to link against — not that runtime is slow. The generated bindings are compiled native code, just as fast as hand-written CPython extensions. Compilation itself is fast because pybind11’s templates are much lighter than Boost’s.
One Thing to Remember
Pybind11 is the go-to tool for new C++/Python binding projects. It combines a clean API, zero dependencies, NumPy integration, and modern C++ support into a package that makes writing Python extensions in C++ feel almost effortless.
See Also
- Python Boost Python Bindings Boost.Python lets C++ code talk to Python using clever C++ tricks, like teaching two people to understand each other through a shared phrasebook.
- Python Buffer Protocol The buffer protocol lets Python objects share raw memory without copying, like passing a notebook around the table instead of photocopying every page.
- Python Capsule Api Python Capsules let C extensions secretly pass pointers to each other through Python, like friends passing a sealed envelope through a mailbox.
- Python Cffi Bindings CFFI lets Python talk to fast C libraries, like giving your app a translator that speaks both languages at the same table.
- Python Extension Modules Api The C Extension API is how Python lets you plug in hand-built C code, like adding a turbo engine under your Python program's hood.