Exceptions¶
Automatic conversion of C++ exceptions¶
When Python calls a C++ function, that function might raise an exception
instead of returning a result. In such a case, nanobind will capture the C++
exception and then raise an equivalent exception within Python. This automatic
conversion supports std::exception
, common subclasses, and several classes
that convert to specific Python exceptions as shown below:
Exception thrown by C++ |
Translated to Python exception type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Any other exception |
|
Exception translation is not bidirectional. A C++ catch
(nb::key_error)
block will not catch a Python KeyError
. Use
nb::python_error
for this purpose (see the example
below for details).
The is also a special exception nb::cast_error
that may
be raised
by the call operator nb::handle::operator()
and nb::cast()
when argument(s)
cannot be converted to Python objects.
Handling custom exceptions¶
nanobind can also expose custom exception types. The
nb::exception<T>
helper resembles
nb::class_<T>
and registers a new exception type within
the provided scope.
NB_MODULE(my_ext, m) {
nb::exception<CppExp>(m, "PyExp");
}
Here, it creates my_ext.PyExp
. Subsequently, any C++ exception of type
CppExp
crossing the language barrier will automatically convert to
my_ext.PyExp
.
A Python exception base class can optionally be specified. For example, the
snippet below causes PyExp
to inherit from RuntimeError
(the default is
Exception
). The built-in Python exception classes are listed here.
nb::exception<CppExp>(module, "PyExp", PyExc_RuntimeError);
In more complex cases, nb::register_exception_translator()
can be called to register a custom exception
translation routine. It takes a stateless callable (e.g. a function pointer or
a lambda function without captured variables) with the call signature
void(const std::exception_ptr &, void*)
and an optional payload pointer
value that will be passed to the second parameter of the callable.
When a C++ exception is captured by nanobind, all registered exception translators are tried in reverse order of registration (i.e. the last registered translator has the first chance of handling the exception).
Inside the translator, call std::rethrow_exception()
within a
try
-catch
block to re-throw the exception and capture supported
exception types. The catch
block should call PyErr_SetString
or
PyErr_Format
(1, 2) to
set a suitable Python error status. The following example demonstrates this
pattern to convert MyCustomException
into a Python IndexError
.
nb::register_exception_translator(
[](const std::exception_ptr &p, void * /* unused */) {
try {
std::rethrow_exception(p);
} catch (const MyCustomException &e) {
PyErr_SetString(PyExc_IndexError, e.what());
}
});
Multiple exceptions can be handled by a single translator. nanobind captures unhandled exceptions and forwards them to the preceding translator. If none of the exception translators succeeds, it will convert according to the previously discussed default rules.
Note
When the exception translator returns normally, it must have set a Python
error status. Otherwise, Python will crash with the message SystemError:
error return without exception set
.
Unsupported exception types should not be caught, or may be explicitly (re-)thrown to delegate them to the other exception translators.
Capturing Python exceptions within C++¶
When nanobind-based C++ code calls a Python function that raises an exception,
it will automatically convert into a nb::python_error
raised on the C++ side. This exception type can be caught and handled in C++ or
propagate back into Python, where it will undergo reverse conversion.
Exception raised in Python |
Translated to C++ exception type |
---|---|
Any Python |
The class exposes various members to obtain further information about the
exception. The .type()
and .value()
methods provide information about the exception type and
value, while .what()
generates a
human-readable representation including a backtrace.
A use of the .matches()
method to
distinguish different exception types is shown below:
try {
nb::object file = nb::module_::import_("io").attr("open")("file.txt", "r");
nb::object text = file.attr("read")();
file.attr("close")();
} catch (const nb::python_error &e) {
if (e.matches(PyExc_FileNotFoundError)) {
nb::print("file.txt not found");
} else if (e.matches(PyExc_PermissionError)) {
nb::print("file.txt found but not accessible");
} else {
throw;
}
}
Note that the previously discussed automatic conversion of C++ exception does not apply here. Errors raised
from Python always convert to nb::python_error
.
Handling errors from the Python C API¶
Whenever possible, use nanobind wrappers instead of calling the Python C API directly. Otherwise, you must carefully manage reference counts and adhere to the nanobind error protocol outlined below.
When a Python C API call fails with an error status, you must immediately
throw nb::python_error();
to capture the error and handle it using
appropriate C++ mechanisms. This includes calls to error setting functions such
as PyErr_SetString
(custom exception translators
are excluded from this rule).
PyErr_SetString(PyExc_TypeError, "C API type error demo");
throw nb::python_error();
// But it would be easier to simply...
throw nb::type_error("nanobind wrapper type error");
Alternately, to ignore the error, call PyErr_Clear(). Any Python error must be thrown or cleared, or nanobind will be left in an invalid state.
Chaining exceptions (‘raise from’)¶
Python has a mechanism for indicating that exceptions were caused by other exceptions:
try:
print(1 / 0)
except Exception as exc:
raise RuntimeError("could not divide by zero") from exc
To do a similar thing in nanobind, you can use the nb::raise_from
function, which requires a nb::python_error
and re-raises it with a chained exception object.
nb::callable f = ...;
int arg = 123;
try {
f(arg);
} catch (nb::python_error &e) {
nb::raise_from(e, PyExc_RuntimeError, "Could not call 'f' with %i", arg);
}
The function is internally based on the Python function PyErr_FormatV
and
takes printf
-style arguments following the format descriptor.
An even lower-level interface is available via nb::chain_error
.
Handling unraisable exceptions¶
If a Python function invoked from a C++ destructor or any function marked
noexcept(true)
(collectively, “noexcept functions”) throws an exception, there
is no way to propagate the exception, as such functions may not throw.
Should they throw or fail to catch any exceptions in their call graph,
the C++ runtime calls std::terminate()
to abort immediately.
Similarly, Python exceptions raised in a class’s __del__
method do not
propagate, but are logged by Python as an unraisable error. In Python 3.8+, a
system hook is triggered
and an auditing event is logged.
Any noexcept function should have a try-catch block that traps
nb::python_error
(or any other exception that can
occur). A useful approach is to convert them to Python exceptions and then
discard_as_unraisable
as shown below.
void nonthrowing_func() noexcept(true) {
try {
// ...
} catch (nb::python_error &e) {
// Discard the Python error using Python APIs, using the C++ magic
// variable __func__. Python already knows the type and value and of the
// exception object.
e.discard_as_unraisable(__func__);
} catch (const std::exception &e) {
// Log and discard C++ exceptions.
third_party::log(e);
}
}