Extending & Embedding Python Using C - Exceptions
Written by Mike James   
Monday, 30 October 2023
Article Index
Extending & Embedding Python Using C - Exceptions
Exception Classes
Raising Exceptions

Exception Classes

As the chain of functions could fail for a wide range of reasons the cause of the exception is signaled by passing an exception object which has properties that define the exception back up the function chain. The except clause can specify what exception objects it can handle. An except clause that can handle a given exception class will also handle all the derived exception objects.

The key idea is that an exception class is defined to handle a particular type of error. When an error occurs and the exception is raised, the system creates an instance of the class which is passed to any exception handlers. The exception handlers can specify the classes that they can handle.

For example, ArithmeticError is designed to be raised for any problem with arithmetic, but it also has three derived classes FloatingPointError, OverflowError and ZeroDivisionError which are actually raised when the specific problem occurs. If you want to handle all three derived cases you can specify ArithmeticError using:

except ArithmeticError:

which is called for any of FloatingPointError, OverflowError and ZeroDivisionError. If you just want to respond to a division-by-zero error you can use:

except ZeroDivisionError:

You can also specify multiple except clauses to deal with different types of exception. If you need to move outside of the exception hierarchy then you can form a group of unrelated exceptions via:

  • exception ExceptionGroup(msg, excs)
  • exception BaseExceptionGroup(msg, excs)

You can also get access to the exception object using as:

  • except ArthmeticError as err

where the exception object has the error message and other details as attributes.

Finally, to raise an exception you use the raise keyword with one of the built-in exception classes, for example:

raise ArithmeticError

If you want to create your own exception class then you simply inherit from the Exception class or one of its derived built-in classes. You can add attributes to the new exception class that provides more information, for example:

class MyNumberError(ArithmeticError):
def __init__(self, message,):
super(MyNumberError, self).__init__(message)

If you now raise this exception:

raise MyNumberError("Number not 42")

and don’t handle the exception then you will see the error:

MyNumberError: Number not 42

There are many more small details about Python’s exception handling. For example, you can unhandle an exception by using raise within a handler. This reactivates the exception and passes it up to the next level.

As well as full exceptions there are also warnings which work in roughly the same way but only display the warning message and don’t stop the program if they are unhandled. You can set filters to suppress warning errors.

Python Exceptions In C

As long as you understand Python exceptions, you should have no problem with supporting them within your C extensions. All of the C API functions make use of the Python exception system. If an error occurs they return either NULL if a pointer is usually returned or -1 if an int is usually returned. The PyArg_ function differ from this and return 1 for success and 0 for failure. This indicates that an error has occurred but the function also sets three pointers that define the exception.

If your C extension function returns NULL or -1 without setting an exception then you will see the Python error message:

SystemError: <built-in function exampleFunction> 
            returned NULL without setting an exception

A function returning an error from the C API has not only to return a NULL or -1, but also set an exception.

If you call a C API function and it returns an error you have the choice of handling the exception or returning it and allowing the Python system to try to find an exception handler. The simplest thing to do is pass the exception to the Python system. Often this means doing nothing at all. For example:

static PyObject* exception1(PyObject *self, 
PyObject *args)
{
PyObject *x = PyLong_FromLong(1);
PyObject *y = PyLong_FromLong(0);
PyObject *ans =PyNumber_TrueDivide(x, y);
Py_DECREF(x);
Py_DECREF(y);
return ans;
}

This function attempts to use TrueDivide to implement an integer division but the function raises a division-by-zero exception and passes back a NULL to indicate that there is an error. The function simply returns the result, a NULL, and the Python system processes the exception. Of course, you might well have to test for the NULL and return early, but generally you don’t have to do any more. You do have to make sure that you clean up any resources used if you do return early, for example:

static PyObject *exception1(PyObject *self, 
PyObject *args)
{
PyObject *x = PyLong_FromLong(1);
PyObject *y = PyLong_FromLong(0);
PyObject *ans = PyNumber_TrueDivide(x, y);
if (ans == NULL)
{
Py_DECREF(x);
Py_DECREF(y);
return NULL;
}
PyObject *z = PyLong_FromLong(1);
PyObject *ans2 = PyNumber_TrueDivide(x, z);
Py_DECREF(x);
Py_DECREF(y);
Py_DECREF(z);
return ans;
}

There is argument for explicitly passing a NULL in the return to make it clear that this is an error return.



Last Updated ( Saturday, 04 November 2023 )