A CPython extension must compile against the target headers, export the matching module initializer, and signal argument errors.
Python C extension: compile, import, and reject bad input
Operation contract
The fixture compiles a small receipt scorer into a temporary extension file, imports that file, doubles a forty-seven-unit amount, and rejects a text argument. The C function checks the amount's range before multiplying, and a failed argument parse returns NULL with an exception already set. The build uses local CPython headers and a platform-specific compiler command.
Failure boundary
This checks one macOS build only. The Linux command is shown but was not run here, and Windows needs another build path. The filename carries the current interpreter's extension suffix; a successful local import does not establish an ABI contract for future versions. Compiler output, binary architecture, system libraries, and interpreter mode all belong to the release matrix.
Working program
import importlib.util
import sys
import sysconfig
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
c_source = r"""
#include <Python.h>
#include <limits.h>
static PyObject *score_receipt(PyObject *self, PyObject *args) {
long amount;
if (!PyArg_ParseTuple(args, "l", &amount)) return NULL;
if (amount < 0 || amount > LONG_MAX / 2) {
PyErr_SetString(PyExc_ValueError, "amount outside contract");
return NULL;
}
return PyLong_FromLong(amount * 2);
}
static PyMethodDef methods[] = {
{"score", score_receipt, METH_VARARGS, "Score a receipt."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT, "receipt_native", NULL, -1, methods
};
PyMODINIT_FUNC PyInit_receipt_native(void) {
return PyModule_Create(&module);
}
"""
with TemporaryDirectory() as directory:
source = Path(directory) / "receipt_native.c"
binary = Path(directory) / ("receipt_native" + sysconfig.get_config_var("EXT_SUFFIX"))
source.write_text(c_source, encoding="utf-8")
command = ["cc"]
if sys.platform == "darwin":
command += ["-bundle", "-undefined", "dynamic_lookup"]
elif sys.platform.startswith("linux"):
command += ["-shared", "-fPIC"]
else:
raise RuntimeError("this fixture needs a POSIX compiler")
command += ["-I" + sysconfig.get_path("include"), str(source), "-o", str(binary)]
subprocess.run(command, check=True, capture_output=True, text=True)
specification = importlib.util.spec_from_file_location("receipt_native", binary)
module = importlib.util.module_from_spec(specification)
specification.loader.exec_module(module)
print("score", module.score(47))
try:
module.score("47")
except TypeError:
print("text_rejected", True)Output
score 94
text_rejected TrueCosts and limits
Compilation dominates this tiny computation. Native calls still cross a Python/C boundary, so the extension needs a measured workload before replacing a pure Python implementation.
Common Mistakes
- A local binary import is not a cross-version or cross-platform test.
- Return NULL only when a Python exception is set on the error path.
- Do not multiply a C long before checking its range.
