Sample code

code/hello

The smallest useful modern extension module: multi-phase init, METH_O and METH_FASTCALL, correct sentinel handling.

Build and run
cd code/hello
python -m pip install -e . --no-build-isolation -v
python -m pytest -q

Sources on disk are under code/hello/ and are compilable as-is. Each section below links to its original file.

Files

hello.c

/*
 * hello -- the smallest useful modern CPython extension module.
 *
 * Demonstrates:
 *   - multi-phase initialization (PEP 489)
 *   - METH_O and METH_FASTCALL calling conventions
 *   - correct error handling for an ambiguous conversion sentinel
 *   - declaring free-threading support
 *
 * Build:  pip install -e . --no-build-isolation
 * Lessons: 01, 05, 06, 08, 09
 */

#include <Python.h>

/* ------------------------------------------------------------------ *
 * add_one(n, /) -> int
 *
 * METH_O: exactly one positional argument, no parsing at all.
 * ------------------------------------------------------------------ */
static PyObject *
hello_add_one(PyObject *module, PyObject *arg)
{
    long v = PyLong_AsLong(arg);
    /* -1 is a legitimate result, so the sentinel alone is not an error. */
    if (v == -1 && PyErr_Occurred()) {
        return NULL;
    }
    if (v == LONG_MAX) {
        PyErr_SetString(PyExc_OverflowError, "result would overflow a C long");
        return NULL;
    }
    return PyLong_FromLong(v + 1);
}

/* ------------------------------------------------------------------ *
 * greet(name, greeting="Hello", /) -> str
 *
 * METH_FASTCALL: arguments arrive as a C array, no tuple is built.
 * The array and its elements are borrowed and valid only for this call.
 * ------------------------------------------------------------------ */
static PyObject *
hello_greet(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
{
    if (nargs < 1 || nargs > 2) {
        PyErr_Format(PyExc_TypeError,
                     "greet() takes 1 or 2 positional arguments but %zd were given",
                     nargs);
        return NULL;
    }
    if (!PyUnicode_Check(args[0])) {
        PyErr_Format(PyExc_TypeError, "name must be str, not %s",
                     Py_TYPE(args[0])->tp_name);
        return NULL;
    }
    if (nargs == 1) {
        return PyUnicode_FromFormat("Hello, %U!", args[0]);
    }
    if (!PyUnicode_Check(args[1])) {
        PyErr_Format(PyExc_TypeError, "greeting must be str, not %s",
                     Py_TYPE(args[1])->tp_name);
        return NULL;
    }
    return PyUnicode_FromFormat("%U, %U!", args[1], args[0]);
}

/* ------------------------------------------------------------------ *
 * total(iterable, /) -> int
 *
 * Shows the abstract API and strict ownership across a loop that can
 * run arbitrary Python code (the iterator's __next__).
 * ------------------------------------------------------------------ */
static PyObject *
hello_total(PyObject *module, PyObject *iterable)
{
    PyObject *iter = PyObject_GetIter(iterable);   /* strong */
    if (iter == NULL) {
        return NULL;
    }

    long long sum = 0;
    PyObject *item;
    while ((item = PyIter_Next(iter)) != NULL) {   /* strong each round */
        long v = PyLong_AsLong(item);
        Py_DECREF(item);
        if (v == -1 && PyErr_Occurred()) {
            Py_DECREF(iter);
            return NULL;
        }
        sum += v;
    }
    Py_DECREF(iter);

    /* PyIter_Next returns NULL both for exhaustion and for an error. */
    if (PyErr_Occurred()) {
        return NULL;
    }
    return PyLong_FromLongLong(sum);
}

/* ------------------------------------------------------------------ */

static PyMethodDef hello_methods[] = {
    {"add_one", hello_add_one, METH_O,
     PyDoc_STR("add_one(n, /)\n--\n\nReturn n + 1.")},

    {"greet", (PyCFunction)(void (*)(void))hello_greet, METH_FASTCALL,
     PyDoc_STR("greet(name, greeting='Hello', /)\n--\n\nReturn a greeting.")},

    {"total", hello_total, METH_O,
     PyDoc_STR("total(iterable, /)\n--\n\nSum an iterable of ints.")},

    {NULL, NULL, 0, NULL}
};

static int
hello_exec(PyObject *module)
{
    if (PyModule_AddStringConstant(module, "__version__", "0.1.0") < 0) {
        return -1;
    }
    return 0;
}

static PyModuleDef_Slot hello_slots[] = {
    {Py_mod_exec, hello_exec},
    {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},
#endif
    {0, NULL}
};

static PyModuleDef hello_module = {
    .m_base    = PyModuleDef_HEAD_INIT,
    .m_name    = "hello",
    .m_doc     = PyDoc_STR("Minimal example extension module."),
    .m_size    = 0,                    /* no per-module state needed */
    .m_methods = hello_methods,
    .m_slots   = hello_slots,
};

PyMODINIT_FUNC
PyInit_hello(void)
{
    return PyModuleDef_Init(&hello_module);
}

Original source: code/hello/hello.c

setup.py

import sys

from setuptools import Extension, setup

if sys.platform == "win32":
    warn_flags = ["/W4"]
else:
    warn_flags = ["-Wall", "-Wextra", "-Wno-unused-parameter"]

setup(
    ext_modules=[
        Extension(
            name="hello",  # must match PyInit_hello
            sources=["hello.c"],
            extra_compile_args=warn_flags,
        ),
    ],
)

Original source: code/hello/setup.py

pyproject.toml

[build-system]
requires = ["setuptools>=77"]
build-backend = "setuptools.build_meta"

[project]
name = "hello"
version = "0.1.0"
description = "Minimal CPython C extension example"
requires-python = ">=3.13"

[tool.pytest.ini_options]
testpaths = ["tests"]

Original source: code/hello/pyproject.toml

tests/test_hello.py

import sys

import pytest

import hello


def test_add_one():
    assert hello.add_one(41) == 42
    assert hello.add_one(-2) == -1  # the ambiguous-sentinel case


def test_add_one_rejects_non_int():
    with pytest.raises(TypeError):
        hello.add_one("41")


def test_add_one_overflow():
    with pytest.raises(OverflowError):
        hello.add_one(2**70)


def test_greet():
    assert hello.greet("Ada") == "Hello, Ada!"
    assert hello.greet("Ada", "Welcome") == "Welcome, Ada!"


@pytest.mark.parametrize("args", [(), ("a", "b", "c")])
def test_greet_arity(args):
    with pytest.raises(TypeError):
        hello.greet(*args)


def test_greet_rejects_non_str():
    with pytest.raises(TypeError):
        hello.greet(1)


def test_total():
    assert hello.total([1, 2, 3]) == 6
    assert hello.total(iter(range(10))) == 45
    assert hello.total([]) == 0


def test_total_propagates_iterator_errors():
    def boom():
        yield 1
        raise RuntimeError("from the iterator")

    with pytest.raises(RuntimeError, match="from the iterator"):
        hello.total(boom())


def test_total_rejects_non_int_items():
    with pytest.raises(TypeError):
        hello.total([1, "two", 3])


@pytest.mark.skipif(
    not getattr(sys, "_is_gil_enabled", lambda: True)(),
    reason="exact refcounts are not meaningful on free-threaded builds",
)
def test_no_reference_leak():
    arg = 12345678
    hello.add_one(arg)
    before = sys.getrefcount(arg)
    for _ in range(1000):
        hello.add_one(arg)
        hello.total([arg])
    assert sys.getrefcount(arg) == before

Original source: code/hello/tests/test_hello.py

README.md

# hello

The smallest useful modern CPython extension module.

```bash
python -m pip install -e . --no-build-isolation -v
python -c "import hello; print(hello.greet('Ada'), hello.add_one(41))"
python -m pytest -q
```

Remember: editing `hello.c` requires a rebuild. Re-run the install command.

Things to try:

1. Rename `PyInit_hello` to `PyInit_helo`, rebuild, and read the import error.
2. Remove the `PyErr_Occurred()` check in `add_one` and call `hello.add_one("x")`.
3. Change `METH_FASTCALL` to `METH_VARARGS` on `greet` and see what breaks.
4. Print `sysconfig.get_config_var("EXT_SUFFIX")` and match it against the built file.

Original source: code/hello/README.md