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— 148 linessetup.py— 19 linespyproject.toml— 13 linestests/test_hello.py— 71 linesREADME.md— 19 lines
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