mrkeyoor.com_
Tue 22 Sept 18:47 UTC
PyPICLI & Toolingupdated 22 Sept 2026

pybind11 review

pybind11 3.1.0 is a set of C++ headers and build helpers for exposing native functions, classes, buffers, exceptions, and callbacks to Python, or embedding Python inside a C++ program. The PyPI wheel is pure Python because it delivers those headers and discovery metadata; the extension you build from them is native and tied to a platform and interpreter ABI. Version 3.1.0 drops Python 3.8 and MSVC 2017, expands strict numeric conversions to match PEP 484, adds reusable subinterpreter thread state, and fixes string_view lifetime handling plus a subinterpreter crash. Our installed helper package imported in 0.08 seconds and included py.typed metadata.

Verdict

pybind11 3.1.0 installed in 0.2 seconds and used 2 MB in our sandbox, but that result covers its headers and helpers rather than the native module you still have to compile and ship. Install it for a hand-designed C++ boundary only when the team owns wheel production, ABI coverage, GIL use, and object lifetimes.

We installed it

Lab card: what happened when we installed pybind11Screenshot of pybind11 documentation
Install✓ · 0.2s1 package on disk · 2 MB
Importimport pybind11 in 0.08s · pure Python · py.typed · requires Python >=3.9
Known vulns0(pip-audit)

Answers from our run

Does pybind11 install cleanly?

Yes. In a fresh container with an empty cache, pip install pybind11 finished in 0.2s, leaving 1 package and 2 MB on disk. pip-audit reported no known vulnerabilities.

What does pybind11 need to run?

Python >=3.9, and nothing compiled: it is pure Python. In our run import pybind11 succeeded in 0.08s, and the package ships py.typed for type checkers.

pybind11 or nanobind: which should you use?

nanobind: Choose it for a pybind11-like C++ binding API when compile time and extension size deserve direct comparison. pybind11 3.1.0 installed in 0.2 seconds and used 2 MB in our sandbox, but that result covers its headers and helpers rather than the native module you still have to compile and ship.

When should you not use pybind11?

There is no C or C++ code to bind and the job is speeding up Python loops; Cython or vectorized NumPy usually creates less packaging work

API stability4/5The module.def, class_, py::arg, exception registration, NumPy buffer, holder, and call_guard forms remain established across the 3.x line. Version 3.1.0 still changes real compatibility boundaries by removing Python 3.8 and MSVC 2017, and it adjusts strict numeric conversions so float accepts int while complex accepts int and float. Binding code should survive an ordinary minor update, but compilers, advanced subinterpreter code, and generated annotations need release-note review.
Docs5/5The versioned Read the Docs manual covers a first extension, CMake discovery, classes, ownership policies, exceptions, callbacks, NumPy, embedding, GIL handling, subinterpreters, smart pointers, and upgrade notes. The repository also points to maintained examples for setuptools, scikit-build, and CMake. Advanced lifetime sections require careful reading because the correct policy depends on the native API, yet the manual names the failure modes and gives focused code.
Maintenance5/5GitHub showed a push on 2026-08-25, and version 3.1.0 was released on 2026-08-06. That release supports current interpreter work, adds subinterpreter thread-state handling, fixes a data race and string_view lifetime faults, corrects cross-compilation behavior, and tests Python 3.15 in CI. GitHub's open count was 713 issues and pull requests combined, which is a large queue, while the recent fixes show that difficult runtime paths still receive attention.
Ecosystem5/5The stored registry count is 7,744,023 weekly downloads, and GitHub reported 18,004 stars during research. pybind11 connects with CMake, scikit-build-core, setuptools, cibuildwheel, auditwheel, conda-forge, and NumPy, and its README names CPython, PyPy, and GraalPy support. That breadth helps with examples and packaging recipes, though every downstream project must still choose and test its own compiler and wheel matrix.

Use it if

  • An existing C++ library needs a deliberately designed Python API for its functions and classes
  • NumPy arrays should reach native code through the buffer protocol with explicit dtype, shape, and copy rules
  • Python subclasses or callbacks must cross into C++, and the team can manage the GIL and object ownership
  • A C++ application needs to embed CPython and call Python code under controlled interpreter lifetime
Skip it if

Setup reality

We installed pybind11 3.1.0 on Python 3.12 in 0.2 seconds. The clean sandbox ended with 1 package and 2 MB on disk, and pip-audit found 0 known vulnerabilities. The measured package declares 1 direct dependency, requires Python 3.9 or newer, is pure Python, and ships py.typed. import pybind11 completed in 0.08 seconds. Its license field was unknown in the installed package metadata.

That quick pip result does not compile an extension. You still need a C++ compiler, Python development headers, and a build backend. CMake projects can use find_package(pybind11 CONFIG REQUIRED), while Python distributions commonly put scikit-build-core and pybind11 in build-system.requires. Build isolation creates a fresh environment, so installing pybind11 in the active virtual environment does not satisfy an omitted build requirement.

A release needs wheels for every Python ABI and platform you promise to support. Linux builds commonly use a manylinux image and auditwheel; macOS and Windows require their own toolchains. If pip cannot find a compatible wheel, it may attempt the source distribution on the user's box. Version 3.1.0 also ends support for Python 3.8 and MSVC 2017, so upgrade those targets before changing the build requirement.

Runtime failures cluster around ownership and the GIL. Automatic STL conversion normally copies. A NumPy forcecast can allocate another array. return_value_policy and keep_alive must match the C++ lifetime, and a native thread must acquire the GIL before touching Python. Release the GIL around long native work only when that work uses no Python objects. Test wheel installation, interpreter shutdown, callbacks, and Python subclass overrides in clean environments, not only the in-tree build.

Patterns

Expose a native function bind-function

#include <pybind11/pybind11.h>
namespace py = pybind11;

int add(int left, int right) { return left + right; }

PYBIND11_MODULE(example, module) {
    module.def("add", &add, py::arg("left"), py::arg("right"));
}

The identifier passed to PYBIND11_MODULE must equal the compiled extension name. A mismatch makes Python look for the wrong initialization symbol.

Create an extension target with CMake configure-cmake

cmake_minimum_required(VERSION 3.15)
project(example LANGUAGES CXX)

find_package(pybind11 CONFIG REQUIRED)
pybind11_add_module(example src/bindings.cpp)
target_compile_features(example PRIVATE cxx_std_17)

find_package needs pybind11's CMake metadata in the build environment. A separate runtime installation does not satisfy an isolated build.

Declare an isolated wheel build configure-pyproject

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

[project]
name = "example"
version = "0.1.0"
requires-python = ">=3.9"

build-system.requires is installed into a fresh build environment. pybind11 3.1.0 itself requires Python 3.9 or newer.

Expose a class and property bind-class

py::class_<Point>(module, "Point")
    .def(py::init<double, double>(), py::arg("x"), py::arg("y"))
    .def_readwrite("x", &Point::x)
    .def_readwrite("y", &Point::y)
    .def_property_readonly("length", &Point::length);

The default holder owns the wrapped C++ object. Supply a compatible holder when instances come from shared or external ownership.

Use a shared pointer holder bind-shared-owner

py::class_<Session, std::shared_ptr<Session>>(module, "Session")
    .def(py::init<std::string>())
    .def("close", &Session::close);

module.def("current_session", &current_session);

A class hierarchy must use compatible holder types throughout. Adding shared_ptr at one binding cannot fix a native API that returns a dangling pointer.

Enable standard container conversion convert-stl

#include <pybind11/stl.h>

std::vector<std::string> names();
std::map<std::string, int> counts();

module.def("names", &names);
module.def("counts", &counts);

pybind11/stl.h converts vectors and maps into Python containers by value in ordinary bindings. Large containers can pay for a full copy.

Read a contiguous NumPy array accept-numpy-array

#include <pybind11/numpy.h>

double total(py::array_t<double, py::array::c_style | py::array::forcecast> input) {
    auto view = input.unchecked<1>();
    double sum = 0;
    for (py::ssize_t i = 0; i < view.shape(0); ++i) sum += view(i);
    return sum;
}

forcecast accepts convertible input by making a compatible array when needed. Remove it if the function must reject a mismatched dtype or memory layout.

Release the GIL around native work release-gil

module.def(
    "simulate",
    &simulate,
    py::arg("steps"),
    py::call_guard<py::gil_scoped_release>()
);

gil_scoped_release covers the entire native call here. simulate must not access Python objects or invoke Python APIs during that interval.

Accept a Python callback call-python-callback

#include <pybind11/functional.h>

void visit(const std::function<void(int)> &callback) {
    for (int value = 0; value < 10; ++value) callback(value);
}

module.def("visit", &visit);

pybind11/functional.h enables std::function conversion. A callback made from a native worker thread requires the GIL before entering Python.

Expose a domain exception translate-exception

class ParseError : public std::runtime_error {
    using std::runtime_error::runtime_error;
};

PYBIND11_MODULE(example, module) {
    py::register_exception<ParseError>(module, "ParseError");
    module.def("parse", &parse);
}

register_exception creates a Python exception for that C++ type. Unrelated exceptions continue through pybind11's standard translation rules.

Tie a child reference to its parent return-borrowed-child

py::class_<Store>(module, "Store")
    .def(
        "item",
        &Store::item,
        py::return_value_policy::reference_internal
    );

reference_internal keeps the Store wrapper alive while the returned item wrapper exists. It cannot prevent Store from invalidating that item internally.

Forward a virtual method to Python support-python-subclass

class PyAnimal : public Animal {
public:
    using Animal::Animal;
    std::string sound() const override {
        PYBIND11_OVERRIDE_PURE(std::string, Animal, sound);
    }
};

py::class_<Animal, PyAnimal>(module, "Animal")
    .def(py::init<>())
    .def("sound", &Animal::sound);

A pure virtual forwarded with PYBIND11_OVERRIDE_PURE raises when the Python subclass omits the method. Each overridable C++ method needs trampoline coverage.

Alternatives

PackageRegistryPick it when
nanobindPyPIChoose it for a pybind11-like C++ binding API when compile time and extension size deserve direct comparison.
CythonPyPIChoose it when Python-shaped code is being compiled or a C API is easier to express in Cython declarations.
cffiPyPIChoose it for a C ABI when C++ templates, holders, and class bindings are unnecessary.
cppyyPyPIChoose it for runtime access to broad C++ APIs when writing a manual extension layer is too costly.

More cli & tooling guides

chalk · commander · typescript · esbuild · yargs · click · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.