File: exceptions.rst

package info (click to toggle)
nanobind 2.9.2-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 3,060 kB
  • sloc: cpp: 11,838; python: 5,862; ansic: 4,820; makefile: 22; sh: 15
file content (291 lines) | stat: -rw-r--r-- 10,941 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
.. cpp:namespace:: nanobind

.. _exceptions:

Exceptions
==========

.. _exception_conversion:

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:

.. list-table::
  :widths: 40 60
  :header-rows: 1

  * - Exception thrown by C++
    - Translated to Python exception type
  * - ``std::exception``
    - ``RuntimeError``
  * - ``std::bad_alloc``
    - ``MemoryError``
  * - ``std::domain_error``
    - ``ValueError``
  * - ``std::invalid_argument``
    - ``ValueError``
  * - ``std::length_error``
    - ``ValueError``
  * - ``std::out_of_range``
    - ``IndexError``
  * - ``std::range_error``
    - ``ValueError``
  * - ``std::overflow_error``
    - ``OverflowError``
  * - :cpp:func:`nb::stop_iteration <stop_iteration>`
    - ``StopIteration`` (used to implement custom iterator) 
  * - :cpp:func:`nb::index_error <index_error>`
    - ``IndexError`` (used to indicate out of bounds access in ``__getitem__``,
      ``__setitem__``, etc.)
  * - :cpp:func:`nb::key_error <index_error>`
    - ``KeyError`` (used to indicate an invalid access in ``__getitem__``,
      ``__setitem__``, etc.)
  * - :cpp:func:`nb::value_error <value_error>`
    - ``ValueError`` (used to indicate an invalid value in operations like
      ``container.remove(...)``)
  * - :cpp:func:`nb::type_error <type_error>`
    - ``TypeError``
  * - :cpp:func:`nb::buffer_error <type_error>`
    - ``BufferError``
  * - :cpp:func:`nb::import_error <import_error>`
    - ``ImportError``
  * - :cpp:func:`nb::attribute_error <attribute_error>`
    - ``AttributeError``
  * - Any other exception
    - ``SystemError``

Exception translation is not bidirectional. A C++ ``catch
(nb::key_error)`` block will not catch a Python ``KeyError``. Use
:cpp:class:`nb::python_error <python_error>` for this purpose (see the :ref:`example
below <handling_python_exceptions_cpp>` for details).

The is also a special exception :cpp:class:`nb::cast_error <cast_error>` that may
be raised
by the call operator :cpp:func:`nb::handle::operator()
<detail::api::operator()>` and :cpp:func:`nb::cast() <cast>` when argument(s)
cannot be converted to Python objects.

.. _custom_exceptions:

Handling custom exceptions
--------------------------

nanobind can also expose custom exception types. The
:cpp:class:`nb::exception\<T\> <exception>` helper resembles
:cpp:class:`nb::class_\<T\> <class_>` and registers a new exception type within
the provided scope.

.. code-block:: cpp

   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
<https://docs.python.org/3/c-api/exceptions.html#standard-exceptions>`__.

.. code-block:: cpp

    nb::exception<CppExp>(module, "PyExp", PyExc_RuntimeError);

In more complex cases, :cpp:func:`nb::register_exception_translator()
<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
<https://docs.python.org/3/c-api/exceptions.html#c.PyErr_SetString>`__, `2
<https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Format>`__) to
set a suitable Python error status. The following example demonstrates this
pattern to convert ``MyCustomException`` into a Python ``IndexError``.

.. code-block:: cpp

    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.

.. _handling_python_exceptions_cpp:

Capturing Python exceptions within C++
--------------------------------------

When nanobind-based C++ code calls a Python function that raises an exception,
it will automatically convert into a :class:`nb::python_error <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.

.. list-table::
  :widths: 40 60
  :header-rows: 1

  * - Exception raised in Python
    - Translated to C++ exception type
  * - Any Python ``Exception``
    - :cpp:class:`nb::python_error <python_error>`

The class exposes various members to obtain further information about the
exception. The :cpp:func:`.type() <python_error::type>` and :cpp:func:`.value()
<python_error::value>` methods provide information about the exception type and
value, while :cpp:func:`.what() <python_error::what>` generates a
human-readable representation including a backtrace.

A use of the :cpp:func:`.matches() <python_error::matches>` method to
distinguish different exception types is shown below:

.. code-block:: cpp

    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 :ref:`automatic conversion
<exception_conversion>` of C++ exception does not apply here. Errors raised
from Python *always* convert to :cpp:class:`nb::python_error <python_error>`.

Handling errors from the Python C API
-------------------------------------

Whenever possible, use :ref:`nanobind wrappers <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`` (:ref:`custom exception translators <custom_exceptions>`
are excluded from this rule).

.. code-block:: cpp

    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()
<https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Clear>`__. Any
Python error must be thrown or cleared, or nanobind will be left in an
invalid state.

.. _exception_chaining:

Chaining exceptions ('raise from')
----------------------------------

Python has a mechanism for indicating that exceptions were caused by other
exceptions:

.. code-block:: py

    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 :cpp:func:`nb::raise_from
<raise_from>` function, which requires a :cpp:class:`nb::python_error
<python_error>` and re-raises it with a chained exception object.

.. code-block:: cpp

    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 :cpp:func:`nb::chain_error
<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
<https://docs.python.org/3/library/sys.html#sys.unraisablehook>`_
and an auditing event is logged.

Any noexcept function should have a try-catch block that traps
:cpp:class:`nb::python_error <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.

.. code-block:: cpp

    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);
        }
    }