Low-level interface

nanobind exposes a low-level interface to provide fine-grained control over the sequence of steps that instantiates a Python object wrapping a C++ instance. This is useful when writing generic binding code that manipulates nanobind-based objects of various types.

Given a previous nb::class_<...> binding declaration, the nb::type<T>() template function can be used to look up the Python type object associated with a C++ class named MyClass.

nb::handle py_type = nb::type<MyClass>();

In the case of failure, this line will return a nullptr pointer, which can be checked via py_type.is_valid(). We can verify that the type lookup succeeded, and that the returned instance indeed represents a nanobind-owned type (via nb::type_check(), which is redundant in this case):

assert(py_type.is_valid() && nb::type_check(py_type));

nanobind knows the size, alignment, and C++ RTTI std::type_info record of all bound types. They can be queried on the fly via nb::type_size(), nb::type_align(), and nb::type_info() in situations where this is useful.

assert(nb::type_size(py_type) == sizeof(MyClass) &&
       nb::type_align(py_type) == alignof(MyClass) &&
       nb::type_info(py_type) == typeid(MyClass));

Given a type object representing a C++ type, we can create an uninitialized instance via nb::inst_alloc(). This is an ordinary Python object that can, however, not (yet) be passed to bound C++ functions to prevent undefined behavior. It must first be initialized.

nb::object py_inst = nb::inst_alloc(py_type);

We can confirm via nb::inst_check() that this newly created instance is managed by nanobind, that it has the correct type in Python. Calling nb::inst_ready() reveals that the ready flag of the instance is set to false (i.e., it is still uninitialized).

assert(nb::inst_check(py_inst) &&
       py_inst.type().is(py_type) &&
       !nb::inst_ready(py_inst));

For simple plain old data (POD) types, the nb::inst_zero() function can be used to zero-initialize the object and mark it as ready.

nb::inst_zero(py_inst);
assert(nb::inst_ready(py_inst));

We can destruct this default instance via nb::inst_destruct() and convert it back to non-ready status. This memory region can then be reinitialized once more.

nb::inst_destruct(py_inst);
assert(!nb::inst_ready(py_inst));

What follows is a more interesting example, where we use a lesser-known feature of C++ (the “placement new” operator) to construct an instance in-place into the memory region allocated by nanobind.

// Get a C++ pointer to the uninitialized instance data
MyClass *ptr = nb::inst_ptr<MyClass>(py_inst);

// Perform an in-place construction of the C++ object at address 'ptr'
new (ptr) MyClass(/* constructor arguments go here */);

Following this constructor call, we must inform nanobind that the instance object is now fully constructed via nb::inst_mark_ready(). When its reference count reaches zero, nanobind will then automatically call the in-place destructor (MyClass::~MyClass).

nb::inst_mark_ready(py_inst);
assert(nb::inst_ready(py_inst));

Let’s destroy this instance once more manually (which will, again, call the C++ destructor and mark the Python object as non-ready).

nb::inst_destruct(py_inst);

Another useful feature is that nanobind can copy- or move-construct py_inst from another instance of the same type via nb::inst_copy() and nb::inst_move(). These functions call the C++ copy or move constructor and transition py_inst back to ready status. This is equivalent to calling an in-place version of these constructors followed by a call to nb::inst_mark_ready() but compiles to more compact code (the nb::class_<MyClass> declaration had already created bindings for both constructors, and this simply calls those bindings).

if (copy_instance)
    nb::inst_copy(/* dst = */ py_inst, /* src = */ some_other_instance);
else
    nb::inst_move(/* dst = */ py_inst, /* src = */ some_other_instance);

Both functions assume that the destination object is uninitialized. Two alternative versions nb::inst_replace_copy() and nb::inst_replace_move() destruct an initialized instance and replace it with the contents of another by either copying or moving.

if (copy_instance)
    nb::inst_replace_copy(/* dst = */ py_inst, /* src = */ some_other_instance);
else
    nb::inst_replace_move(/* dst = */ py_inst, /* src = */ some_other_instance);

Note that these functions are all unsafe in the sense that they do not verify that their input arguments are valid. This is done for performance reasons, and such checks (if needed) are therefore the responsibility of the caller. Functions labeled nb::type_* should only be called with nanobind type objects, and functions labeled nb::inst_* should only be called with nanobind instance objects.

The functions nb::type_check() and nb::inst_check() are exceptions to this rule: they accept any Python object and test whether something is a nanobind type or instance object.

Two further functions nb::type_name() and nb::inst_name() determine the type name associated with a type or instance thereof. These also accept non-nanobind types and instances.

The function nb::inst_python_derived() reports whether a nanobind instance belongs to a Python subclass of the bound type rather than to the bound type itself.

Even lower-level interface

Every nanobind object has two important flags that control its behavior:

  1. ready: is the object fully constructed? If set to false, nanobind will raise an exception when the object is passed to a bound C++ function.

  2. destruct: Should nanobind call the C++ destructor when the instance is garbage collected?

The functions nb::inst_zero(), nb::inst_mark_ready(), nb::inst_move(), and nb::inst_copy() set both of these flags to true, and nb::inst_destruct() sets both of them to false.

In rare situations, the destructor should not be invoked when the instance is garbage collected, for example when working with a nanobind instance representing a field of a parent instance created using the nb::rv_policy::reference_internal return value policy. The library therefore exposes two more functions nb::inst_state() and nb::inst_set_state() that can be used to access them individually.

Referencing existing instances

The above examples used the function nb::inst_alloc() to allocate a Python object along with space to hold a C++ instance associated with the binding py_type.

nb::object py_inst = nb::inst_alloc(py_type);

// Next, perform a C++ in-place construction into the
// address given by nb::inst_ptr<MyClass>(py_inst)
... omitted, see the previous examples ...

What if the C++ instance already exists? nanobind also supports this case via the nb::inst_reference() and nb::inst_take_ownership() functions—in this case, the Python object references the existing memory region, which is potentially (slightly) less efficient due to the need for an extra indirection.

MyClass *inst = new MyClass();

// Transfer ownership of 'inst' to Python (which will use a delete
// expression to free it when the Python instance is garbage collected)
nb::object py_inst = nb::inst_take_ownership(py_type, inst);

// We can also wrap C++ instances that should not be destructed since
// they represent offsets into another data structure. In this case,
// the optional 'parent' parameter ensures that 'py_inst' remains alive
// while 'py_subinst' exists to prevent undefined behavior.
nb::object py_subinst = nb::inst_reference(
    py_field_type, &inst->field, /* parent = */ py_inst);

Supplemental type data

nanobind can stash supplemental data inside the type object of bound types. This involves the nb::supplement<T>() class binding annotation to reserve space and nb::type_supplement<T>() to access the reserved memory region.

An example use of this fairly advanced feature are libraries that register large numbers of different types (e.g. flavors of tensors). A single generically implemented function can then query the supplemental data block to handle each tensor type slightly differently.

Here is what this might look like in an implementation:

struct MyTensorMetadata {
    bool stored_on_gpu;
    // ..
    // should be a POD (plain old data) type
};

// Register a new type MyTensor, and reserve space for sizeof(MyTensorMedadata)
nb::class_<MyTensor> cls(m, "MyTensor", nb::supplement<MyTensorMedadata>())

/// Mutable reference to 'MyTensorMedadata' portion in Python type object
MyTensorMedadata &supplement = nb::type_supplement<MyTensorMedadata>(cls);
supplement.stored_on_gpu = true;

The nb::supplement<T>() annotation implicitly also passes nb::is_final() to ensure that type objects with supplemental data cannot be subclassed in Python.

nanobind requires that the specified type T be trivially default constructible. It zero-initializes the supplement when the type is first created but does not perform any further custom initialization or destruction. You can fill the supplement with different contents following the type creation, e.g., using the placement new operator.

The contents of the supplemental data are not directly visible to Python’s cyclic garbage collector, which creates challenges if you want to reference Python objects. The recommended workaround is to store the Python objects as attributes of the type object (in its __dict__) and store a borrowed PyObject* reference in the supplemental data. If you use an attribute name that begins with the symbol @, then nanobind will prevent Python code from rebinding or deleting the attribute after it has been set, making the borrowed reference reasonably safe.

Type namespaces

Python exposes the namespace of a type object through the read-only proxy cls.__dict__. The function nb::type_dict() returns the underlying dictionary, which is useful to inspect or modify the attributes that a type declares itself.

nb::dict d = nb::type_dict(cls);

// Does 'cls' declare 'my_method', or does it inherit the attribute?
bool local = d.contains("my_method");

Modifications of this dictionary bypass the __setattr__ protocol, in which case the attribute caches of the interpreter must be flushed manually.

d["my_attribute"] = value;

// Announce the change to the interpreter
PyType_Modified((PyTypeObject *) cls.ptr());

When Python evaluates an attribute access like inst.my_property, it first searches the type of inst and its base classes for an entry of that name. This search visits the bases in method resolution order (MRO). The instance dictionary and the descriptor protocol then determine the final result, e.g., by replacing a property by its current value.

The function nb::type_lookup() performs only this first step and returns the raw entry, which reveals what a type provides without involving an instance or running any of the associated machinery.

// Fetch the descriptor of a static property without invoking its getter
nb::object o = nb::type_lookup(cls, "my_property");

The search skips the metaclass, hence attributes like __name__ that a type object inherits from type are not found. It returns an invalid object (checked via o.is_valid()) when there is no matching entry, and it never raises an exception.

Instance dictionaries

The function nb::inst_dict() provides access to the instance dictionary of an arbitrary Python object. This is more efficient than the equivalent expression isnt.attr("__dict__"). The function never raises but may return an object o with !o.is_valid() when of h lacks an attribute dictionary.

nb::dict d = nb::inst_dict(inst);
if (d.is_valid())
    d["my_attribute"] = value;

A type bound via nb::class_ must declare nb::dynamic_attr to equip its instances with such a dictionary. Python subclasses including trampolines always have one.

Customizing type creation

nanobind exposes a low-level interface to install custom type slots (PyType_Slot in the CPython API) in newly constructed types. This provides an escape hatch to realize features that were not foreseen in the design of this library.

To use this feature, specify the nb::type_slots() annotation when creating the type.

nb::class_<MyClass>(m, "MyClass", nb::type_slots(slots));

Here, slots should refer to an array of function pointers that are tagged with a corresponding slot identifier. For example, here is an example function that overrides the addition operator so that it behaves like a multiplication.

PyObject *myclass_tp_add(PyObject *a, PyObject *b) {
    return PyNumber_Multiply(a, b);
}

PyType_Slot slots[] = {
    { Py_nb_add, (void *) myclass_tp_add },
    { 0, nullptr }
};

The slots array specified in the previous nb::class_<MyClass>() declaration references the function myclass_tp_add and is followed by a mandatory null terminator. Information on type slots can be found in the CPython documentation sections covering type objects and type construction.

This example is contrived because it could have been accomplished using builtin features:

nb::class_<MyClass>(m, "MyClass")
    .def("__add__",
         [](const MyClass &a, const MyClass &b) { return a * b; },
         nb::is_operator())

The documentation section on reference leaks discusses another important use case of type slots.