Porting guide#

The API of nanobind is extremely similar to that of pybind11, which makes porting existing projects easy. Parts of the nanobind documentation are almost verbatim copies pybind11’s documentation.

A number of noteworthy changes are documented below.

Namespace#

nanobind types and functions are located in the nanobind namespace. The following shorthand alias is recommended and used throughout the documentation:

namespace nb = nanobind;

Name changes#

The following macros, types, and functions were renamed:

pybind11

nanobind

PYBIND11_MODULE(..)

NB_MODULE(..)

PYBIND11_OVERRIDE_*(..)

NB_OVERRIDE_*(..)

error_already_set

python_error

type::of<T>()

type<T>()

type

type_object

reinterpret_borrow<T>(x)

borrow<T>(x)

reinterpret_steal<T>(x)

steal<T>(x)

.def_readwrite(..)

.def_rw(..)

.def_readonly(..)

.def_ro(..)

.def_property(..)

.def_prop_rw(..)

.def_property_readonly(..)

.def_prop_ro(..)

.def_readwrite_static(..)

.def_rw_static(..)

.def_readonly_static(..)

.def_ro_static(..)

.def_property_static(..)

.def_prop_rw_static(..)

.def_property_readonly_static(..)

.def_prop_ro_static(..)

register_exception<T>

exception<T>

None/null arguments#

In contrast to pybind11, nanobind does not permit None-valued function arguments by default. You must enable them explicitly using the arg::none() argument annotation, e.g.:

m.def("func", &func, "arg"_a.none());

It is also possible to set a None default value, in which case the .none() annotation can be omitted.

m.def("func", &func, "arg"_a = nb::none());

None-valued arguments are only supported by two of the three parameter passing styles described in the section on information exchange. In particular, they are supported by bindings and wrappers, but not by type casters.

Shared pointers and holders#

When nanobind instantiates a C++ type within Python, the resulting instance data is stored within the created Python object (”PyObject”). Alternatively, when an already existing C++ instance is transferred to Python via a function return value and rv_policy::reference, rv_policy::reference_internal, or rv_policy::take_ownership, nanobind creates a smaller wrapper PyObject that only stores a pointer to the instance data.

This is very different from pybind11, where the instance PyObject contained a holder type (typically std::unique_ptr<T>) storing a pointer to the instance data. Dealing with holders caused inefficiencies and introduced complexity; they were therefore removed in nanobind. This has implications on object ownership, shared ownership, and interactions with C++ shared/unique pointers. The intermediate and advanced sections on object ownership provide further detail.

The gist is that it is no longer necessary to specify holder types in the type declaration:

pybind11:

py::class_<MyType, std::shared_ptr<MyType>>(m, "MyType")

nanobind:

nb::class_<MyType>(m, "MyType")

To bind functions that exchange shared/unique pointers, you must add one or both of the following include directives to your code:

#include <nanobind/stl/unique_ptr.h>
#include <nanobind/stl/shared_ptr.h>

Binding functions that take std::unique_ptr<T> arguments involves some limitations that can be avoided by changing their signatures to std::unique_ptr<T, nb::deleter<T>> (details).

Use of std::enable_shared_from_this<T> is permitted, but since nanobind does not use holder types, an object constructed in Python will typically not have any associated std::shared_ptr<T> until it is passed to a C++ function that accepts std::shared_ptr<T>. That means a C++ function that accepts a raw T* and calls shared_from_this() on it might stop working when ported from pybind11 to nanobind. You can solve this problem by always passing such objects across the Python/C++ boundary as std::shared_ptr<T> rather than as T*. See the advanced section on object ownership for more details.

Custom constructors#

In pybind11, custom constructors (i.e. ones that do not already exist in the C++ class) could be specified as a lambda function returning an instance of the desired type.

py::class_<MyType>(m, "MyType")
    .def(py::init([](int) { return MyType(...); }));

Unfortunately, the implementation of this feature was quite complex and often required further internal calls to the move or copy constructor. nanobind instead reverts to how pybind11 originally implemented this feature using in-place construction (placement new):

nb::class_<MyType>(m, "MyType")
    .def("__init__", [](MyType *t) { new (t) MyType(...); });

The provided lambda function will be called with a pointer to uninitialized memory that has already been allocated (this memory region is co-located with the Python object for reasons of efficiency). The lambda function can then either run an in-place constructor and return normally (in which case the instance is assumed to be correctly constructed) or fail by raising an exception.

To turn an existing factory function into a constructor, you will need to combine the above pattern with an invocation of the move/copy-constructor, e.g.:

nb::class_<MyType>(m, "MyType")
    .def("__init__", [](MyType *t) { new (t) MyType(MyType::create()); });

Implicit conversions#

In pybind11, implicit conversions were specified using a follow-up function call. This also works in nanobind, but it is recommended that you already specify them within the constructor declaration:

pybind11:

py::class_<MyType>(m, "MyType")
    .def(py::init<MyOtherType>());

py::implicitly_convertible<MyOtherType, MyType>();

nanobind:

nb::class_<MyType>(m, "MyType")
    .def(nb::init_implicit<MyOtherType>());

Trampoline classes#

Trampolines, i.e., polymorphic class implementations that forward virtual function calls to Python, now require an extra NB_TRAMPOLINE(parent, size) declaration, where parent refers to the parent class and size is at least as big as the number of NB_OVERRIDE_*() calls. nanobind caches information to enable efficient function dispatch, for which it must know the number of trampoline “slots”.

The macro PYBIND11_OVERRIDE_*(..) required the base type and return value as the first two arguments. This information is no longer needed in nanobind, and the arguments should be removed in NB_OVERRIDE_*():

An example:

struct PyAnimal : Animal {
    NB_TRAMPOLINE(Animal, 1);

    std::string name() const override {
        NB_OVERRIDE(name);
    }
};

Trampoline declarations with an insufficient size may eventually trigger a Python RuntimeError exception with a descriptive label, e.g.:

nanobind::detail::get_trampoline('PyAnimal::what()'): the trampoline ran out of
slots (you will need to increase the value provided to the NB_TRAMPOLINE() macro)

Iterator bindings#

Use of the nb::make_iterator(), nb::make_key_iterator(), and nb::make_value_iterator() functions requires including the additional header file nanobind/make_iterator.h. The interface of these functions has also slightly changed: all take a Python scope and a name as first and second arguments, which are used to permanently “install” the iterator type (which is created on demand). See the test suite for a worked out example.

Type casters#

The API of custom type casters has changed significantly. The following changes are needed:

  • load() was renamed to from_python(). The function now takes an extra uint8_t flags parameter (instead bool convert, which is now represented by the flag nb::detail::cast_flags::convert). A cleanup_list * pointer keeps track of Python temporaries that are created by the conversion, and which need to be deallocated after a function call has taken place.

    flags and cleanup should be passed to any recursive usage of type_caster::from_python(). If casting fails due to a Python exception, the function should clear it (PyErr_Clear()) and return false. If a severe error condition arises that should be reported, use Python warning API calls for this, e.g. PyErr_WarnFormat().

  • cast() was renamed to from_cpp(). The function takes a return value policy (as before) and a cleanup_list * pointer. If casting fails due to a Python exception, the function should leave the error set (note the asymmetry compared to from_python()) and return nullptr.

Note that the cleanup list is only available when from_python() or from_cpp() are called as part of function dispatch, while usage by nb::cast() may set cleanup to nullptr if implicit conversions are not enabled. This case should be handled gracefully by refusing the conversion if the cleanup list is absolutely required.

Type casters may not raise C++ exceptions. Both from_python() and from_cpp() must be annotated with noexcept. Exceptions or failure conditions caused by a conversion should instead be caught within the function body and handled as follows:

  • from_python(): return false. That’s it. (Failed Python to C++ conversion occur all the time while dispatching calls, causing nanobind to simply move to the next function overload. Dedicated error reporting would add undesirable overheads). In case of a severe internal error, use the CPython warning API (e.g., PyErr_Warn()) to notify the user.

  • from_cpp(): a failure here is more serious, since a return value of a successfully evaluated cannot be converted, causing the call to fail. To provide further detail, use the CPython error API (e.g., PyErr_Format()) and return an invalid handle (return nb::handle();).

The std::pair<T1, T2> type caster (link) may be useful as a starting point of custom implementations.

Removed features#

A number of pybind11 features are unavailable in nanobind. The list below uses the following symbols:

  • ○: This removal is a design choice. Use pybind11 if you need this feature.

  • ●: Unclear, to be discussed.

Removed features include:

  • Multiple inheritance: this feature was a persistent source of complexity in pybind11 and it is one of the main casualties in creating nanobind.

  • Holders: nanobind instances co-locate instance data with a Python object instead of accessing it via a holder type. This is a major difference compared to pybind11 and will require changes to binding code that used custom holders (e.g. unique or shared pointers). The intermediate and advanced sections on object ownership provide further detail.

  • Multi-intepreter, Embedding: The ability to embed Python in an executable or run several independent Python interpreters in the same process is unsupported. Nanobind caters to bindings only. Multi-interpreter support would require TLS lookups for nanobind data structures, which is undesirable.

  • Function binding annotations: The pos_only argument annotation was removed. However, the same behavior can be achieved by creating unnamed arguments; see the discussion in the section on keyword-only arguments.

  • Metaclasses: creating types with custom metaclasses is unsupported.

  • Module-local bindings: support was removed (both for types and exceptions).

  • Custom allocation: C++ classes with an overloaded or deleted operator new / operator delete are not supported.

  • Compilation: workarounds for buggy or non-standard-compliant compilers were removed and will not be reintroduced.

  • ○ The options class for customizing docstring generation was removed.

  • ○ The NumPy array class (py::array) was removed in exchange for a more powerful alternative (nb::ndarray<..>) that additionally supports CPU/GPU tensors produced by various frameworks (NumPy, PyTorch, TensorFlow, JAX, etc.). Its API is not compatible with pybind11, however.

  • ● Buffer protocol binding (.def_buffer()) was removed in favor of nb::ndarray<..>.

  • ● Support for evaluating Python files was removed.

Bullet points marked with ● may be reintroduced eventually, but this will need to be done in a careful opt-in manner that does not affect code complexity, binary size, and compilation/runtime performance of basic bindings that don’t depend on these features.