Search code examples
pythonerror-handlingpytestcpythonpython-c-api

Raising exception in init causes SystemError: returned a result with an error set in Python C API


I am using pytest to test my own Python C extension module. I am trying to check if the TypeError occurs properly when an argument of invalid type is input to the __init__ method. The method implementation is something like

PyObject * myObject_init(myObject *self, PyObject *args)
{
    if ("# args are invalid")
    {
        PyErr_SetString(PyExc_TypeError, "Invalid Argument");
        return NULL;
    }
}

This makes TypeError occur. But the problem is that when I test this method with pytest like,

def test_init_with_invalid_argument():
    x = "something invalid"
    with pytest.raises(TypeError):
        obj = MyObject(x)

it does fail. The Error message is something like

TypeError: Invalid Argument

The above exception was the direct cause of the following exception:

self = <test_mymodule.TestMyObjectInit object at 0x00000239886D27F0>

    def test_init_with_invalid_argument(self):
        with pytest.raises(TypeError):
>           obj = MyObject(x)
E           SystemError: <class 'mymodule.MyObject'> returned a result with an error set

tests\test_init_with_invalid_argument.py:19: SystemError

What is the problem here, and how can I make the test pass?


Solution

  • Your __init__ function has the wrong signature.

    The __init__ method is defined by the tp_init slot of a PyTypeObject, which if set needs to be an initproc, i.e. a function with the signature

    int tp_init(PyObject *self, PyObject *args, PyObject *kwds)
    

    Note that this function returns an int, not a PyObject* like ordinary methods.

    The return value should be 0 when initialization succeeds and -1 when initialization fails and an error is set. Note that this is flip-flopped from the behavior of ordinary methods, which return 0 (NULL) on failure and a non-NULL pointer on success. Your function is following the behavior of ordinary methods by returning NULL, but this is the exact opposite of what init needs to do.

    Change your init function to return an int, and replace return NULL with return -1. Also, make sure the happy path returns 0 (as opposed to, say, self), and add the missing PyObject *kwds to the argument list.