When writing C extension/interpreter code, treat every fallible C API call as a potential failure point and handle it consistently:
PyNumber_Subtract, PyLong_AsLong, PyObject_GetAttrString, etc., check for failure before using the result. Don’t assume a prior check covers a later re-invocation of the same API.PyLong_AsLong/PyLong_AsInt where a valid value can coincide with the sentinel, check value == -1 && PyErr_Occurred() rather than a bare PyErr_Occurred(), so you don’t misinterpret a stale error flag or a legitimate -1 result.PyIter_NextItem over PyIter_Next) to avoid ambiguity between “no result” and “error”.if/loop condition. This keeps error propagation explicit and consistent with tier-2/JIT expectations.assert. Values coming from arbitrary Python objects (e.g. a fold or year attribute) must be range-checked with real error handling (PyErr_Format(PyExc_ValueError, ...)), since assert can be compiled out and casts to narrower types are undefined behavior if the value is out of range.ValueError escape where callers only catch RuntimeError).PyList_SetSlice/extend-like calls that steal references on failure, don’t additionally Py_DECREF; add an assert/comment to document why).Example of correct failure-check idiom and resource cleanup:
long fold_int = PyLong_AsLong(fold_obj);
Py_DECREF(fold_obj);
if (fold_int == -1 && PyErr_Occurred()) {
return NULL;
}
if (fold_int < 0 || fold_int > 1) {
PyErr_Format(PyExc_ValueError, "fold must be 0 or 1, got %ld", fold_int);
return NULL;
}
fold = (unsigned char)fold_int;
Applying this consistently prevents silent data corruption from unchecked UB casts, memory leaks/double-frees on error paths, and confusing failures where errors are swallowed, misclassified, or propagate inconsistently.