Skip to content

Commit 0a8fe10

Browse files
committed
Add an error handling tutorial.
1 parent 17529d2 commit 0a8fe10

3 files changed

Lines changed: 255 additions & 3 deletions

File tree

Doc/extending/error-handling.rst

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
.. highlight:: c
2+
3+
4+
.. _error-handling:
5+
6+
7+
***************************
8+
Error handling in the C API
9+
***************************
10+
11+
This chapter covers the details about how Python's C API expresses errors
12+
and how to interact with Python exceptions.
13+
14+
The exception indicator
15+
=======================
16+
17+
Python has a thread-local indicator for the state of the current exception.
18+
This indicator is just a ``PyObject *`` referencing an instance of
19+
:class:`BaseException`. You can think of this like the ``errno`` variable in C.
20+
21+
If a C API function fails, it may set the exception indicator to a Python
22+
exception object. For example, creating a new object may fail and set the
23+
exception indicator to a :class:`MemoryError` object to denote that an
24+
allocation failed.
25+
26+
Generally speaking, you must not call functions with the exception indicator
27+
set. This is explained in more detail later on.
28+
29+
30+
The failure protocol
31+
====================
32+
33+
In the C API, ``NULL`` is never a valid ``PyObject *``, so it is used as a
34+
sentinel to indicate failure for functions that return a ``PyObject *``.
35+
In fact, we've already used this! Going back to our ``system`` function,
36+
we can see this in action:
37+
38+
.. code-block:: c
39+
40+
:emphasize-lines: 6
41+
42+
static PyObject *
43+
spam_system(PyObject *self, PyObject *arg)
44+
{
45+
const char *command = PyUnicode_AsUTF8(arg);
46+
if (command == NULL) {
47+
return NULL;
48+
}
49+
int status = system(command);
50+
PyObject *result = PyLong_FromLong(status);
51+
return result;
52+
}
53+
54+
55+
``spam_system`` returns a ``PyObject *``, so we indicate failure by returning
56+
``NULL``. To expand on this, let's try to modify ``spam_system`` to raise an
57+
exception if the result is non-zero:
58+
59+
.. code-block:: c
60+
:emphasize-lines: 6
61+
62+
static PyObject *
63+
spam_system(PyObject *self, PyObject *arg)
64+
{
65+
const char *command = PyUnicode_AsUTF8(arg);
66+
if (command == NULL) {
67+
return NULL;
68+
}
69+
int status = system(command);
70+
if (status != 0) {
71+
return NULL;
72+
}
73+
74+
// We don't know how to return None yet, so let's do this for now.
75+
return PyLong_FromLong(status);
76+
}
77+
78+
Because ``system`` is not from Python's C API, it has no knowledge of Python's
79+
exception indicator, and thus does not set any exceptions. So, if we were to
80+
run this code with an invalid command, the interpreter would raise a
81+
:class:`SystemError`:
82+
83+
.. code-block:: pycon
84+
85+
>>> import spam
86+
>>> result = spam.system('noexist')
87+
SystemError: <built-in function system> returned NULL without setting an exception
88+
89+
To manually raise an exception, we can use :c:func:`PyErr_SetString`, which
90+
will take a reference to an exception class and a C string to use as the
91+
message. All of Python's built-in exceptions are available as global C
92+
variables prefixed with ``PyExc_`` followed by their name in Python.
93+
For example, :class:`RuntimeError` is available as :c:var:`PyExc_RuntimeError`.
94+
The full list is available at :ref:`standardexceptions`.
95+
96+
With this knowledge, let's make our function raise a ``RuntimeError`` upon
97+
failure:
98+
99+
.. code-block:: c
100+
:emphasize-lines: 10
101+
102+
static PyObject *
103+
spam_system(PyObject *self, PyObject *arg)
104+
{
105+
const char *command = PyUnicode_AsUTF8(arg);
106+
if (command == NULL) {
107+
return NULL;
108+
}
109+
int status = system(command);
110+
if (status != 0) {
111+
PyErr_SetString(PyExc_RuntimeError, "system() call failed");
112+
return NULL;
113+
}
114+
115+
// We don't know how to return None yet, so let's do this for now.
116+
return PyLong_FromLong(status);
117+
}
118+
119+
Now, if we run this:
120+
121+
.. code-block:: pycon
122+
123+
>>> import spam
124+
>>> result = spam.system('noexist')
125+
RuntimeError: system() call failed
126+
127+
128+
Yay! But, this isn't a very descriptive error message. It'd be nice if users
129+
of ``system`` knew exactly what went wrong when invoking their command.
130+
131+
We can provide do this by using :c:func:`PyErr_Format`, which takes a format
132+
string following by variadic arguments instead of a single constant string.
133+
This is similar to ``printf`` in C. Let's try it:
134+
135+
.. code-block:: c
136+
:emphasize-lines: 10-11
137+
138+
static PyObject *
139+
spam_system(PyObject *self, PyObject *arg)
140+
{
141+
const char *command = PyUnicode_AsUTF8(arg);
142+
if (command == NULL) {
143+
return NULL;
144+
}
145+
int status = system(command);
146+
if (status != 0) {
147+
PyErr_Format(PyExc_RuntimeError,
148+
"system() returned non-zero exit code %d", status);
149+
return NULL;
150+
}
151+
152+
// We don't know how to return None yet, so let's do this for now.
153+
return PyLong_FromLong(status);
154+
}
155+
156+
157+
And if we try it, everything works as expected:
158+
159+
160+
.. code-block:: pycon
161+
162+
>>> import spam
163+
>>> result = spam.system('noexist')
164+
RuntimeError: system() returned non-zero exit code 127
165+
166+
167+
But, our function still returns ``0`` if it succeeds, which is now useless.
168+
Ideally, we should return ``None``, like a normal Python function would.
169+
Our first instinct might be to return ``NULL``, so let's try it:
170+
171+
.. code-block:: c
172+
:emphasize-lines: 15
173+
174+
static PyObject *
175+
spam_system(PyObject *self, PyObject *arg)
176+
{
177+
const char *command = PyUnicode_AsUTF8(arg);
178+
if (command == NULL) {
179+
return NULL;
180+
}
181+
int status = system(command);
182+
if (status != 0) {
183+
PyErr_Format(PyExc_RuntimeError,
184+
"system() returned non-zero exit code %d", status);
185+
return NULL;
186+
}
187+
188+
return NULL;
189+
}
190+
191+
.. code-block:: pycon
192+
193+
>>> import spam
194+
>>> spam.system('true')
195+
SystemError: <built-in function system> returned NULL without setting an exception
196+
197+
198+
Nope -- again, ``NULL`` is reserved for exceptions. In Python, ``None`` is still
199+
an object, so we have to return a reference to it. We can do this by returning
200+
a strong reference to :c:var:`Py_None`:
201+
202+
203+
.. code-block:: c
204+
:emphasize-lines: 16
205+
206+
static PyObject *
207+
spam_system(PyObject *self, PyObject *arg)
208+
{
209+
const char *command = PyUnicode_AsUTF8(arg);
210+
if (command == NULL) {
211+
return NULL;
212+
}
213+
int status = system(command);
214+
if (status != 0) {
215+
PyErr_Format(PyExc_RuntimeError,
216+
"system() returned non-zero exit code %d", status);
217+
return NULL;
218+
}
219+
220+
// Py_NewRef() is just a shorthand for Py_INCREF() with an expression
221+
return Py_NewRef(Py_None);
222+
}
223+
224+
.. note::
225+
226+
In CPython, :const:`None` is actually an :term:`immortal` object, meaning
227+
that it has a fixed reference count and is never deallocated, and thus
228+
``Py_INCREF`` has no real effect here.
229+
230+
231+
In fact, this is so common that the C API has a macro for it:
232+
233+
234+
.. code-block:: c
235+
:emphasize-lines: 15
236+
237+
static PyObject *
238+
spam_system(PyObject *self, PyObject *arg)
239+
{
240+
const char *command = PyUnicode_AsUTF8(arg);
241+
if (command == NULL) {
242+
return NULL;
243+
}
244+
int status = system(command);
245+
if (status != 0) {
246+
PyErr_Format(PyExc_RuntimeError,
247+
"system() returned non-zero exit code %d", status);
248+
return NULL;
249+
}
250+
251+
Py_RETURN_NONE;
252+
}

Doc/extending/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ as part of this version of CPython.
7878

7979
#. :ref:`first-extension-module`
8080
#. :ref:`reference-counting-intro`
81+
#. :ref:`error-handling`
8182

8283

8384
Guides for intermediate topics

Doc/extending/reference-counting.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
An introduction to reference counting
99
*************************************
1010

11+
This chapter covers the basics of CPython's garbage collection scheme.
12+
1113
What is reference counting?
1214
===========================
1315

@@ -71,7 +73,6 @@ To understand how this works in practice, let's go back to our ``system``
7173
function, taking note of ``PyObject *`` uses this time:
7274

7375
.. code-block:: c
74-
7576
:emphasize-lines: 1-2, 9
7677
7778
static PyObject *
@@ -117,7 +118,6 @@ from it.
117118
To visualize:
118119

119120
.. code-block:: c
120-
121121
:emphasize-lines: 4-8, 10
122122
123123
static PyObject *
@@ -155,7 +155,6 @@ For example, let's add a bug to ``spam_system`` where we release a borrowed
155155
reference:
156156

157157
.. code-block:: c
158-
159158
:emphasize-lines: 5
160159
161160
static PyObject *

0 commit comments

Comments
 (0)