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.
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
| Install | ✓ · 0.2s | 1 package on disk · 2 MB |
| Import | ✓ | import pybind11 in 0.08s · pure Python · py.typed · requires Python >=3.9 |
| Known vulns | 0 | (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
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
- 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
- You cannot publish wheels for each supported operating system, CPU, Python ABI, and C library target; source installs hand the compiler problem to users
- Python 3.8 or MSVC 2017 remains in support, since pybind11 3.1.0 removed both from its tested baseline
- The boundary depends on a stable binary across ordinary Python releases; standard extension modules remain specific to interpreter and platform ABIs
- Nobody can audit return policies, holders, callback threads, and borrowed references; a bad lifetime choice can end in a process crash instead of a Python exception
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", ¤t_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
| Package | Registry | Pick it when |
|---|---|---|
| nanobind | PyPI | Choose it for a pybind11-like C++ binding API when compile time and extension size deserve direct comparison. |
| Cython | PyPI | Choose it when Python-shaped code is being compiled or a C API is easier to express in Cython declarations. |
| cffi | PyPI | Choose it for a C ABI when C++ templates, holders, and class bindings are unnecessary. |
| cppyy | PyPI | Choose 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.

