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

Featurepybind11
No dependenciesHeader-only; just include the headers
Fast compilationLighter templates than Boost.Python
Modern C++Uses C++11/14/17 features naturally
NumPy supportBuilt-in, zero-copy array access
STL supportAutomatic conversion for vector, map, set, optional, variant
Lambda supportBind lambdas directly as Python functions

Build Options

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.

pythonpybind11cppnative-extensionsnumpy

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.