4. A Pythonic Coding Pattern for C Functions

To avoid all the errors we have seen it is useful to have a C coding pattern for handling PyObjects that does the following:

  • No early returns and a single place for clean up code.

  • Borrowed references incref’d and decref’d correctly.

  • No stale Exception from previous execution path.

  • Exceptions set and tested.

  • NULL is returned when an exception is set.

  • Non-NULL is returned when no exception is set.

The basic pattern in C is similar to Python’s try/except/finally pattern:

try:
    /* Do fabulous stuff here. */
except:
    /* Handle every abnormal condition and clean up. */
finally:
    /* Clean up under normal conditions and return an appropriate value. */

Firstly we set any local PyObject (s) and the return value to NULL:

static PyObject *function(PyObject *arg_1) {
    PyObject *obj_a    = NULL;
    PyObject *ret      = NULL;

Then we have a little bit of Pythonic C - this can be omitted:

    goto try; /* Pythonic 'C' ;-) */
try:

Check that there are no lingering Exceptions:

assert(! PyErr_Occurred());

An alternative check for no lingering Exceptions:

if(PyErr_Occurred()) {
    goto except;
}

Now we assume that any argument is a “Borrowed” reference so we increment it (we need a matching Py_DECREF before function exit, see below). The first pattern assumes a non-NULL argument.

assert(arg_1);
Py_INCREF(arg_1);

If you are willing to accept NULL arguments then this pattern would be more suitable:

if (arg_1) {
    Py_INCREF(arg_1);
}

Of course the same test must be used when calling Py_DECREF, or just use Py_XDECREF.

Now we create any local objects, if they are “Borrowed” references we need to incref them. With any abnormal behaviour we do a local jump straight to the cleanup code.

/* Local object creation. */
/* obj_a = ...; */
if (! obj_a) {
    PyErr_SetString(PyExc_ValueError, "Ooops.");
    goto except;
}
/* If obj_a is a borrowed reference rather than a new reference. */
Py_INCREF(obj_a);

Create the return value and deal with abnormal behaviour in the same way:

/* More of your code to do stuff with arg_1 and obj_a. */
/* Return object creation, ret should be a new reference otherwise you are in trouble. */
/* ret = ...; */
if (! ret) {
    PyErr_SetString(PyExc_ValueError, "Ooops again.");
    goto except;
}

You might want to check the contents of the return value here. On error jump to except: otherwise jump to finally:.

/* Any return value checking here. */

/* If success then check exception is clear,
 * then goto finally; with non-NULL return value. */
assert(! PyErr_Occurred());
assert(ret);
goto finally;

This is the except block where we cleanup any local objects and set the return value to NULL.

except:
    /* Failure so Py_XDECREF the return value here. */
    Py_XDECREF(ret);
    /* Check a Python error is set somewhere above. */
    assert(PyErr_Occurred());
    /* Signal failure. */
    ret = NULL;

Notice the except: block falls through to the finally: block.

finally:
    /* All _local_ PyObjects declared at the entry point are Py_XDECREF'd here.
     * For new references this will free them. For borrowed references this
     * will return them to their previous refcount.
     */
    Py_XDECREF(obj_a);
    /* Decrement the ref count of externally supplied the arguments here.
     * If you allow arg_1 == NULL then Py_XDECREF(arg_1). */
    Py_DECREF(arg_1);
    /* And return...*/
    return ret;
}

Here is the complete code with minimal comments:

static PyObject *function(PyObject *arg_1) {
    PyObject *obj_a    = NULL;
    PyObject *ret      = NULL;

    goto try;
try:
    assert(! PyErr_Occurred());
    assert(arg_1);
    Py_INCREF(arg_1);

    /* obj_a = ...; */
    if (! obj_a) {
        PyErr_SetString(PyExc_ValueError, "Ooops.");
        goto except;
    }
    /* Only do this if obj_a is a borrowed reference. */
    Py_INCREF(obj_a);

    /* More of your code to do stuff with obj_a. */

    /* Return object creation, ret must be a new reference. */
    /* ret = ...; */
    if (! ret) {
        PyErr_SetString(PyExc_ValueError, "Ooops again.");
        goto except;
    }
    assert(! PyErr_Occurred());
    assert(ret);
    goto finally;
except:
    Py_XDECREF(ret);
    assert(PyErr_Occurred());
    ret = NULL;
finally:
    /* Only do this if obj_a is a borrowed reference. */
    Py_XDECREF(obj_a);
    Py_DECREF(arg_1);
    return ret;
}