18. pybind11: Use C++ libraries#

pybind11 lets you write Python extensions using pure C++; no special tool or processing step needed. It’s just a header-only library that works just about everywhere. Used by SciPy, PyTorch, and many more libraries.

18.1. Example#

A Python extension in pybind11 looks like this:


#include <pybind11/pybind11.h>

namespace py = pybind11;

int square(int x) {
    return x * x;
}

PYBIND11_MODULE(somecode, m) {
    m.def("square", &square);
}

You can use cppimport to import it for a quick test, or a build system like scikit-build-core to build. I’m not including a compiler in this environment, so I’m not going to build here - see one of my other classes. This is a minimal CMakeLists.txt:


cmake_minimium_required(VERSION 3.15...3.26)
project(python_example LANGUAGES CXX)

set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 CONFIG REQUIRED)

pybind11_add_module(python_example MODULE src/main.cpp)

install(TARGETS python_example DESTINATION .)

And, your pyproject.toml:


[build-system]
requires = ["scikit-build-core", "pybind11"]
build-backend = "scikit_build_core.build"

[project]
name = "example"
version = "0.0.1"
requires-python = ">=3.8"

If you want to build and distribute, use cibuildwheel, which is used by Scikit-Learn, Matplotlib, MyPy, and many more; it can be setup for Linux, macOS, and Windows and all common CPython and PyPy versions in just 13 lines:


on: [push, pull_request]

jobs:
  build_wheels:
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v3
        
      - uses: pypa/cibuildwheel@v2.14

      - uses: actions/upload-artifact@v3
        with:
          path: ./wheelhouse/*.whl

18.2. So much more#

Some examples of classes:

#include <pybind11/operators.h>

using namespace pybind11::literals;

PYBIND11_MODULE(example, m) {
    py::class_<Vector>(m, "Vector")
        .def(py::init<double, double>())
        .def_property("x", &Vector::getX, &Vector::setX)
        .def_property("y", &Vector::getY, &Vector::setY)
        .def("mag", &Vector::mag, "I am a mag function")
    
        .def("unit", [](const Vector& self){return self.unit();})
    
        .def("__str__", [](const Vector& self){return py::str("[{}, {}]").format(self.getX(), self.getY());})
        
        .def(py::self *= float())
        .def(float() * py::self)
        .def(py::self * float())
}

You can use lambda functions almost anywhere, and you can ask for py::object or the C++ type interchangeably, or cast between them.