Skip to content

Commit b886369

Browse files
authored
Improve documentation of Python and C++ exceptions (#2408)
The main change is to treat error_already_set as a separate category of exception that arises in different circumstances and needs to be handled differently. The asymmetry between Python and C++ exceptions is further emphasized.
1 parent c58f7b7 commit b886369

File tree

3 files changed

+141
-37
lines changed

3 files changed

+141
-37
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,4 @@ sosize-*.txt
3838
pybind11Config*.cmake
3939
pybind11Targets.cmake
4040
/*env*
41+
/.vscode

docs/advanced/exceptions.rst

Lines changed: 130 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
11
Exceptions
22
##########
33

4-
Built-in exception translation
5-
==============================
4+
Built-in C++ to Python exception translation
5+
============================================
6+
7+
When Python calls C++ code through pybind11, pybind11 provides a C++ exception handler
8+
that will trap C++ exceptions, translate them to the corresponding Python exception,
9+
and raise them so that Python code can handle them.
610

7-
When C++ code invoked from Python throws an ``std::exception``, it is
8-
automatically converted into a Python ``Exception``. pybind11 defines multiple
9-
special exception classes that will map to different types of Python
10-
exceptions:
11+
pybind11 defines translations for ``std::exception`` and its standard
12+
subclasses, and several special exception classes that translate to specific
13+
Python exceptions. Note that these are not actually Python exceptions, so they
14+
cannot be examined using the Python C API. Instead, they are pure C++ objects
15+
that pybind11 will translate the corresponding Python exception when they arrive
16+
at its exception handler.
1117

1218
.. tabularcolumns:: |p{0.5\textwidth}|p{0.45\textwidth}|
1319

1420
+--------------------------------------+--------------------------------------+
15-
| C++ exception type | Python exception type |
21+
| Exception thrown by C++ | Translated to Python exception type |
1622
+======================================+======================================+
1723
| :class:`std::exception` | ``RuntimeError`` |
1824
+--------------------------------------+--------------------------------------+
@@ -46,22 +52,11 @@ exceptions:
4652
| | ``__setitem__`` in dict-like |
4753
| | objects, etc.) |
4854
+--------------------------------------+--------------------------------------+
49-
| :class:`pybind11::error_already_set` | Indicates that the Python exception |
50-
| | flag has already been set via Python |
51-
| | API calls from C++ code; this C++ |
52-
| | exception is used to propagate such |
53-
| | a Python exception back to Python. |
54-
+--------------------------------------+--------------------------------------+
5555

56-
When a Python function invoked from C++ throws an exception, pybind11 will convert
57-
it into a C++ exception of type :class:`error_already_set` whose string payload
58-
contains a textual summary. If you call the Python C-API directly, and it
59-
returns an error, you should ``throw py::error_already_set();``, which allows
60-
pybind11 to deal with the exception and pass it back to the Python interpreter.
61-
(Another option is to call ``PyErr_Clear`` in the
62-
`Python C-API <https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Clear>`_
63-
to clear the error. The Python error must be thrown or cleared, or Python/pybind11
64-
will be left in an invalid state.)
56+
Exception translation is not bidirectional. That is, *catching* the C++
57+
exceptions defined above above will not trap exceptions that originate from
58+
Python. For that, catch :class:`pybind11::error_already_set`. See :ref:`below
59+
<handling_python_exceptions_cpp>` for further details.
6560

6661
There is also a special exception :class:`cast_error` that is thrown by
6762
:func:`handle::call` when the input arguments cannot be converted to Python
@@ -106,7 +101,6 @@ and use this in the associated exception translator (note: it is often useful
106101
to make this a static declaration when using it inside a lambda expression
107102
without requiring capturing).
108103

109-
110104
The following example demonstrates this for a hypothetical exception classes
111105
``MyCustomException`` and ``OtherException``: the first is translated to a
112106
custom python exception ``MyCustomError``, while the second is translated to a
@@ -140,7 +134,7 @@ section.
140134

141135
.. note::
142136

143-
You must call either ``PyErr_SetString`` or a custom exception's call
137+
Call either ``PyErr_SetString`` or a custom exception's call
144138
operator (``exc(string)``) for every exception caught in a custom exception
145139
translator. Failure to do so will cause Python to crash with ``SystemError:
146140
error return without exception set``.
@@ -149,27 +143,128 @@ section.
149143
may be explicitly (re-)thrown to delegate it to the other,
150144
previously-declared existing exception translators.
151145

146+
.. _handling_python_exceptions_cpp:
147+
148+
Handling exceptions from Python in C++
149+
======================================
150+
151+
When C++ calls Python functions, such as in a callback function or when
152+
manipulating Python objects, and Python raises an ``Exception``, pybind11
153+
converts the Python exception into a C++ exception of type
154+
:class:`pybind11::error_already_set` whose payload contains a C++ string textual
155+
summary and the actual Python exception. ``error_already_set`` is used to
156+
propagate Python exception back to Python (or possibly, handle them in C++).
157+
158+
.. tabularcolumns:: |p{0.5\textwidth}|p{0.45\textwidth}|
159+
160+
+--------------------------------------+--------------------------------------+
161+
| Exception raised in Python | Thrown as C++ exception type |
162+
+======================================+======================================+
163+
| Any Python ``Exception`` | :class:`pybind11::error_already_set` |
164+
+--------------------------------------+--------------------------------------+
165+
166+
For example:
167+
168+
.. code-block:: cpp
169+
170+
try {
171+
// open("missing.txt", "r")
172+
auto file = py::module::import("io").attr("open")("missing.txt", "r");
173+
auto text = file.attr("read")();
174+
file.attr("close")();
175+
} catch (py::error_already_set &e) {
176+
if (e.matches(PyExc_FileNotFoundError)) {
177+
py::print("missing.txt not found");
178+
} else if (e.match(PyExc_PermissionError)) {
179+
py::print("missing.txt found but not accessible");
180+
} else {
181+
throw;
182+
}
183+
}
184+
185+
Note that C++ to Python exception translation does not apply here, since that is
186+
a method for translating C++ exceptions to Python, not vice versa. The error raised
187+
from Python is always ``error_already_set``.
188+
189+
This example illustrates this behavior:
190+
191+
.. code-block:: cpp
192+
193+
try {
194+
py::eval("raise ValueError('The Ring')");
195+
} catch (py::value_error &boromir) {
196+
// Boromir never gets the ring
197+
assert(false);
198+
} catch (py::error_already_set &frodo) {
199+
// Frodo gets the ring
200+
py::print("I will take the ring");
201+
}
202+
203+
try {
204+
// py::value_error is a request for pybind11 to raise a Python exception
205+
throw py::value_error("The ball");
206+
} catch (py::error_already_set &cat) {
207+
// cat won't catch the ball since
208+
// py::value_error is not a Python exception
209+
assert(false);
210+
} catch (py::value_error &dog) {
211+
// dog will catch the ball
212+
py::print("Run Spot run");
213+
throw; // Throw it again (pybind11 will raise ValueError)
214+
}
215+
216+
Handling errors from the Python C API
217+
=====================================
218+
219+
Where possible, use :ref:`pybind11 wrappers <wrappers>` instead of calling
220+
the Python C API directly. When calling the Python C API directly, in
221+
addition to manually managing reference counts, one must follow the pybind11
222+
error protocol, which is outlined here.
223+
224+
After calling the Python C API, if Python returns an error,
225+
``throw py::error_already_set();``, which allows pybind11 to deal with the
226+
exception and pass it back to the Python interpreter. This includes calls to
227+
the error setting functions such as ``PyErr_SetString``.
228+
229+
.. code-block:: cpp
230+
231+
PyErr_SetString(PyExc_TypeError, "C API type error demo");
232+
throw py::error_already_set();
233+
234+
// But it would be easier to simply...
235+
throw py::type_error("pybind11 wrapper type error");
236+
237+
Alternately, to ignore the error, call `PyErr_Clear
238+
<https://docs.python.org/3/c-api/exceptions.html#c.PyErr_Clear>`_.
239+
240+
Any Python error must be thrown or cleared, or Python/pybind11 will be left in
241+
an invalid state.
242+
152243
.. _unraisable_exceptions:
153244

154245
Handling unraisable exceptions
155246
==============================
156247

157248
If a Python function invoked from a C++ destructor or any function marked
158249
``noexcept(true)`` (collectively, "noexcept functions") throws an exception, there
159-
is no way to propagate the exception, as such functions may not throw at
160-
run-time.
250+
is no way to propagate the exception, as such functions may not throw.
251+
Should they throw or fail to catch any exceptions in their call graph,
252+
the C++ runtime calls ``std::terminate()`` to abort immediately.
161253

162-
Neither Python nor C++ allow exceptions raised in a noexcept function to propagate. In
163-
Python, an exception raised in a class's ``__del__`` method is logged as an
164-
unraisable error. In Python 3.8+, a system hook is triggered and an auditing
165-
event is logged. In C++, ``std::terminate()`` is called to abort immediately.
254+
Similarly, Python exceptions raised in a class's ``__del__`` method do not
255+
propagate, but are logged by Python as an unraisable error. In Python 3.8+, a
256+
`system hook is triggered
257+
<https://docs.python.org/3/library/sys.html#sys.unraisablehook>`_
258+
and an auditing event is logged.
166259

167260
Any noexcept function should have a try-catch block that traps
168-
class:`error_already_set` (or any other exception that can occur). Note that pybind11
169-
wrappers around Python exceptions such as :class:`pybind11::value_error` are *not*
170-
Python exceptions; they are C++ exceptions that pybind11 catches and converts to
171-
Python exceptions. Noexcept functions cannot propagate these exceptions either.
172-
You can convert them to Python exceptions and then discard as unraisable.
261+
class:`error_already_set` (or any other exception that can occur). Note that
262+
pybind11 wrappers around Python exceptions such as
263+
:class:`pybind11::value_error` are *not* Python exceptions; they are C++
264+
exceptions that pybind11 catches and converts to Python exceptions. Noexcept
265+
functions cannot propagate these exceptions either. A useful approach is to
266+
convert them to Python exceptions and then ``discard_as_unraisable`` as shown
267+
below.
173268

174269
.. code-block:: cpp
175270
@@ -183,8 +278,6 @@ You can convert them to Python exceptions and then discard as unraisable.
183278
eas.discard_as_unraisable(__func__);
184279
} catch (const std::exception &e) {
185280
// Log and discard C++ exceptions.
186-
// (We cannot use discard_as_unraisable, since we have a generic C++
187-
// exception, not an exception that originated from Python.)
188281
third_party::log(e);
189282
}
190283
}

docs/advanced/pycpp/object.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Python types
22
############
33

4+
.. _wrappers:
5+
46
Available wrappers
57
==================
68

@@ -168,3 +170,11 @@ Generalized unpacking according to PEP448_ is also supported:
168170
Python functions from C++, including keywords arguments and unpacking.
169171

170172
.. _PEP448: https://www.python.org/dev/peps/pep-0448/
173+
174+
Handling exceptions
175+
===================
176+
177+
Python exceptions from wrapper classes will be thrown as a ``py::error_already_set``.
178+
See :ref:`Handling exceptions from Python in C++
179+
<handling_python_exceptions_cpp>` for more information on handling exceptions
180+
raised when calling C++ wrapper classes.

0 commit comments

Comments
 (0)