Error handling in the C API¶
This chapter covers the details about how Python’s C API expresses errors and how to interact with Python exceptions.
The exception indicator¶
Python has a thread-local indicator for the state of the current exception.
This indicator is a PyObject * referencing an instance of
BaseException. You can think of this like the errno variable in C.
If a C API function fails, it may set the exception indicator to a Python
exception object. For example, creating a new object may fail and set the
exception indicator to a MemoryError object to denote that an
allocation failed.
Generally speaking, you must not call functions with the exception indicator set. This is explained in more detail later on.
The failure protocol¶
In the C API, NULL is never a valid PyObject *, so it is used as a
sentinel to indicate failure for functions that return a PyObject *.
In fact, we’ve already used this! Going back to our system function,
we can see this in action:
:emphasize-lines: 6
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
PyObject *result = PyLong_FromLong(status);
return result;
}
spam_system returns a PyObject *, so we indicate failure by returning
NULL.
Note
Some functions in the C API return an int instead of a reference, so they
cannot use NULL for failure. These functions will usually return -1
for failure, and 0 otherwise.
To expand on this, let’s try to modify spam_system to raise an
exception if the result is non-zero:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
if (status != 0) {
return NULL;
}
// We don't know how to return None yet, so let's do this for now.
return PyLong_FromLong(status);
}
Because system is not from Python’s C API, it has no knowledge of Python’s
exception indicator, and thus does not set any exceptions. So, if we were to
run this code with an invalid command, the interpreter would raise a
SystemError:
>>> import spam
>>> result = spam.system('noexist')
SystemError: <built-in function system> returned NULL without setting an exception
To manually raise an exception, we can use PyErr_SetString(), which
will take a reference to an exception class and a C string to use as the
message. All of Python’s built-in exceptions are available as global C
variables prefixed with PyExc_ followed by their name in Python.
For example, RuntimeError is available as PyExc_RuntimeError.
The full list is available at Exception and warning types.
With this knowledge, let’s make our function raise a RuntimeError upon
failure:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
if (status != 0) {
PyErr_SetString(PyExc_RuntimeError, "system() call failed");
return NULL;
}
// We don't know how to return None yet, so let's do this for now.
return PyLong_FromLong(status);
}
Now, if we run this:
>>> import spam
>>> result = spam.system('noexist')
RuntimeError: system() call failed
Yay! But, this isn’t a very descriptive error message. It’d be nice if users
of system knew exactly what went wrong when invoking their command.
We can provide do this by using PyErr_Format(), which takes a format
string following by variadic arguments instead of a single constant string.
This is similar to printf in C. Let’s try it:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
if (status != 0) {
PyErr_Format(PyExc_RuntimeError,
"system() returned non-zero exit code %d", status);
return NULL;
}
// We don't know how to return None yet, so let's do this for now.
return PyLong_FromLong(status);
}
And if we try it, everything works as expected:
>>> import spam
>>> result = spam.system('noexist')
RuntimeError: system() returned non-zero exit code 127
But, our function still returns 0 if it succeeds, which is now useless.
Ideally, we should return None, like a normal Python function would.
Our first instinct might be to return NULL, so let’s try it:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
if (status != 0) {
PyErr_Format(PyExc_RuntimeError,
"system() returned non-zero exit code %d", status);
return NULL;
}
return NULL;
}
>>> import spam
>>> spam.system('true')
SystemError: <built-in function system> returned NULL without setting an exception
Nope – again, NULL is reserved for exceptions. In Python, None is still
an object, so we have to return a reference to it. We can do this by returning
a strong reference to Py_None:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
if (status != 0) {
PyErr_Format(PyExc_RuntimeError,
"system() returned non-zero exit code %d", status);
return NULL;
}
// Py_NewRef() is just a shorthand for Py_INCREF() with an expression
return Py_NewRef(Py_None);
}
Note
In CPython, None is actually an immortal object, meaning
that it has a fixed reference count and is never deallocated, and thus
Py_INCREF has no real effect here.
In fact, this is so common that the C API has a macro for it:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
if (status != 0) {
PyErr_Format(PyExc_RuntimeError,
"system() returned non-zero exit code %d", status);
return NULL;
}
Py_RETURN_NONE;
}