Lesson 08

Module Mechanics and Glue Code

Everything between import mymod and your first line of real C: the entry point, the module definition, multi-phase initialization, and per-module state.

Learning objective

You can write the complete, correct boilerplate for a modern extension module from memory or from a template you understand line by line, including per-module state, heap types, and the slots that declare what your module supports. This is the lesson to hand to a coding agent as a specification.

What import does to a shared library

  1. The import machinery finds a file matching the module name — on CPython 3.14, something like mymod.cpython-314-x86_64-linux-gnu.so, mymod.cp314-win_amd64.pyd, or the ABI-neutral mymod.abi3.so.
  2. It loads the shared library (dlopen / LoadLibrary).
  3. It looks up one exported symbol: PyInit_<name>, where <name> is the last dotted component. For pkg.sub.mymod, the symbol is PyInit_mymod.
  4. It calls that function and expects back either a module object (legacy single-phase) or a PyModuleDef object (modern multi-phase), which it then turns into a module.
Name mismatches are the classic first failure

If the built file is named foo but the entry point is PyInit_bar, import fails with ImportError: dynamic module does not define module export function (PyInit_foo). The extension name in your build configuration, the PyInit_ suffix, and m_name should all agree. (Strictly, only the first two must; but a mismatched m_name breaks pickling and repr.)

PyMODINIT_FUNC

PyMODINIT_FUNC expands to PyObject * plus whatever visibility attributes the platform needs — __declspec(dllexport) on Windows, default visibility elsewhere. Use it and never write the return type by hand, or your symbol may not be exported and the import will fail with the message above even though the code is right.

Single-phase versus multi-phase initialization

The original design had PyInit_foo create the module and populate it, all in one call. PEP 489 split that into two phases, and the split is what makes modern isolation possible.

Single-phase (legacy)Multi-phase (PEP 489, use this)
PyInit_x returnsA fully built module objectPyModuleDef_Init(&def) — just the definition
Module created byYou, via PyModule_CreateThe import machinery, which then calls your Py_mod_exec
Per-interpreter instancesOne, shared and cachedOne per interpreter, properly isolated
Works with subinterpretersNoYes
Can declare free-threading supportOnly via PyUnstable_Module_SetGILYes, with the Py_mod_gil slot
Interacts correctly with importlibPartiallyYes (it participates in the spec/loader protocol)

Everything below is multi-phase. There is no good reason to write a new single-phase module.

PyModuleDef, field by field

typedef struct PyModuleDef {
    PyModuleDef_Base   m_base;      /* always PyModuleDef_HEAD_INIT */
    const char        *m_name;      /* "mymod" */
    const char        *m_doc;       /* module docstring, or NULL */
    Py_ssize_t         m_size;      /* bytes of per-module state; 0 if none */
    PyMethodDef       *m_methods;   /* module-level functions, or NULL */
    PyModuleDef_Slot  *m_slots;     /* multi-phase slots; NULL means single-phase */
    traverseproc       m_traverse;  /* GC: visit PyObject* in module state */
    inquiry            m_clear;     /* GC: clear PyObject* in module state */
    freefunc           m_free;      /* release non-Python resources in state */
} PyModuleDef;

m_size is the field that matters most:

The slots

SlotValue / signaturePurpose
Py_mod_execint exec(PyObject *module)Populate the module. May appear more than once; called in order. Return 0 or −1.
Py_mod_createPyObject *create(PyObject *spec, PyModuleDef *def)Rarely needed. Only if you want a module object of a custom type.
Py_mod_multiple_interpretersPy_MOD_MULTIPLE_INTERPRETERS_SUPPORTED (default), ..._NOT_SUPPORTED, or Py_MOD_PER_INTERPRETER_GIL_SUPPORTEDDeclares subinterpreter support. Be honest: if you use C globals, say NOT_SUPPORTED.
Py_mod_gilPy_MOD_GIL_NOT_USED or Py_MOD_GIL_USED (default)Python 3.13+. Declares that the module is safe without the GIL. Lesson 11.

Per-module state, and why globals are wrong

The natural C instinct is a file-scope static:

static PyObject *MyError;          /* WRONG */
static PyTypeObject *CounterType;  /* WRONG */

There is one of those per process, but a module can be instantiated more than once per process:

The fix is to put everything in a struct attached to the module object:

typedef struct {
    PyObject *Error;        /* our exception type    */
    PyObject *CounterType;  /* our heap type         */
    PyObject *cache;        /* a dict we keep around */
} mymod_state;

static inline mymod_state *
get_state(PyObject *module)
{
    void *s = PyModule_GetState(module);
    assert(s != NULL);
    return (mymod_state *)s;
}

And because that struct holds PyObject * fields, the module participates in cycle collection exactly like a type does (Lesson 04):

static int
mymod_traverse(PyObject *module, visitproc visit, void *arg)
{
    mymod_state *st = get_state(module);
    Py_VISIT(st->Error);
    Py_VISIT(st->CounterType);
    Py_VISIT(st->cache);
    return 0;
}

static int
mymod_clear(PyObject *module)
{
    mymod_state *st = get_state(module);
    Py_CLEAR(st->Error);
    Py_CLEAR(st->CounterType);
    Py_CLEAR(st->cache);
    return 0;
}

static void
mymod_free(void *module)
{
    (void)mymod_clear((PyObject *)module);   /* plus any malloc'd resources */
}

Reaching the state from three places

FromHow
A module-level function self is the module: mymod_state *st = get_state(self);
A method on a heap type Declare it METH_METHOD | METH_FASTCALL | METH_KEYWORDS to receive defining_class, then PyType_GetModuleState(defining_class).
A slot function (tp_new, nb_add, …) that gets no class PyObject *m = PyType_GetModuleByDef(Py_TYPE(self), &mymod_def); then PyModule_GetState(m). Returns a borrowed reference.
Why not just use the defining class everywhere

Use PyType_GetModuleState(defining_class) rather than PyType_GetModuleState(Py_TYPE(self)). If a Python subclass of your type exists, Py_TYPE(self) is that subclass, which was not created by your module and has no module attached. defining_class is the class where the method was defined — always yours.

Populating the module

PyModule_AddObjectRef(m, "name", obj)Adds without stealing. Recommended. Returns 0 / −1.
PyModule_Add(m, "name", obj)Steals the reference always, even on error. Convenient for a freshly created temporary.
PyModule_AddObject(m, "name", obj)Legacy. Steals only on success, so error handling is a trap. Avoid.
PyModule_AddIntConstant, PyModule_AddStringConstantConstants from C values.
PyModule_AddIntMacro(m, FLAG_X)Adds FLAG_X under its own name. Ideal for exporting a C library's constants.
PyModule_AddType(m, &StaticType)Calls PyType_Ready and adds under the name after the last dot in tp_name. Static types only.

The complete skeleton

This is the template. Every line is load-bearing.

#include <Python.h>

/* ---- per-module state ------------------------------------------------- */

typedef struct {
    PyObject *Error;
    PyObject *CounterType;
} mymod_state;

static inline mymod_state *get_state(PyObject *m) {
    void *s = PyModule_GetState(m);
    assert(s != NULL);
    return (mymod_state *)s;
}

/* ---- a module-level function ------------------------------------------ */

static PyObject *
mymod_scale(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
{
    if (nargs != 2) {
        PyErr_SetString(PyExc_TypeError, "scale() takes exactly 2 arguments");
        return NULL;
    }
    double x = PyFloat_AsDouble(args[0]);
    if (x == -1.0 && PyErr_Occurred()) return NULL;
    double k = PyFloat_AsDouble(args[1]);
    if (k == -1.0 && PyErr_Occurred()) return NULL;

    if (k == 0.0) {
        mymod_state *st = get_state(module);       /* self IS the module */
        PyErr_SetString(st->Error, "scale factor must be non-zero");
        return NULL;
    }
    return PyFloat_FromDouble(x * k);
}

static PyMethodDef mymod_methods[] = {
    {"scale", (PyCFunction)(void(*)(void))mymod_scale, METH_FASTCALL,
     PyDoc_STR("scale(x, k, /)\n--\n\nReturn x * k.")},
    {NULL, NULL, 0, NULL}
};

/* ---- GC hooks for the state ------------------------------------------- */

static int mymod_traverse(PyObject *m, visitproc visit, void *arg) {
    mymod_state *st = get_state(m);
    Py_VISIT(st->Error);
    Py_VISIT(st->CounterType);
    return 0;
}
static int mymod_clear(PyObject *m) {
    mymod_state *st = get_state(m);
    Py_CLEAR(st->Error);
    Py_CLEAR(st->CounterType);
    return 0;
}
static void mymod_free(void *m) { (void)mymod_clear((PyObject *)m); }

/* ---- phase 2: populate the module ------------------------------------- */

static int
mymod_exec(PyObject *module)
{
    mymod_state *st = get_state(module);

    st->Error = PyErr_NewExceptionWithDoc("mymod.Error", "mymod errors.", NULL, NULL);
    if (st->Error == NULL) return -1;
    if (PyModule_AddObjectRef(module, "Error", st->Error) < 0) return -1;

    /* A heap type bound to THIS module instance (Lesson 03). */
    st->CounterType = PyType_FromModuleAndSpec(module, &Counter_spec, NULL);
    if (st->CounterType == NULL) return -1;
    if (PyModule_AddObjectRef(module, "Counter", st->CounterType) < 0) return -1;

    if (PyModule_AddIntConstant(module, "VERSION", 3) < 0) return -1;
    return 0;
}

static PyModuleDef_Slot mymod_slots[] = {
    {Py_mod_exec, mymod_exec},
    {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
#if PY_VERSION_HEX >= 0x030D0000
    {Py_mod_gil, Py_MOD_GIL_NOT_USED},           /* Lesson 11 */
#endif
    {0, NULL}
};

static PyModuleDef mymod_def = {
    .m_base     = PyModuleDef_HEAD_INIT,
    .m_name     = "mymod",
    .m_doc      = PyDoc_STR("Example extension module."),
    .m_size     = sizeof(mymod_state),
    .m_methods  = mymod_methods,
    .m_slots    = mymod_slots,
    .m_traverse = mymod_traverse,
    .m_clear    = mymod_clear,
    .m_free     = mymod_free,
};

/* ---- phase 1: the entry point ----------------------------------------- */

PyMODINIT_FUNC
PyInit_mymod(void)
{
    return PyModuleDef_Init(&mymod_def);
}

A complete modern module. code/counter/ in this course is a runnable version with a heap type filled in.

Rules for the exec function

Packages, submodules, and naming

An extension inside a package is just a module whose full dotted name ends in the extension's name. If you build mypkg._speedups, the file lands next to mypkg/__init__.py and the entry point is PyInit__speedups — note the two underscores, one from the naming convention and one from the prefix. m_name should be the full dotted name "mypkg._speedups", and tp_name for types inside it likewise "mypkg._speedups.Counter".

The common layout is a thin Python package that imports from a private C module, so you can keep pure-Python conveniences (docstrings, __repr__ helpers, a fallback implementation) in Python:

# mypkg/__init__.py
try:
    from ._speedups import scale, Counter
except ImportError:                       # pure-Python fallback
    from ._pure import scale, Counter
__all__ = ["scale", "Counter"]

On the horizon: PEP 793

PEP 793 introduces PyModExport, an alternative entry point that returns an array of slots directly, with no statically allocated PyModuleDef object at all. Its main motivation is letting a single compiled file work with both the default and the free-threaded build. It targets Python 3.15, and PyInit_* becomes soft-deprecated — fully supported and documented, but not gaining new features. Nothing in this lesson becomes wrong; be aware that a second spelling is arriving.

Hands-on

Without writing C, observe the machinery from the outside:

import sys, math, importlib.util

# Extension modules have no __file__-based source and a different spec origin.
print(math.__spec__)
print(math.__spec__.origin)

# Which stdlib modules are C extensions rather than Python?
print(sorted(sys.builtin_module_names)[:15])

# Multi-phase modules report a real loader that participates in the spec protocol:
spec = importlib.util.find_spec("_socket")
print(spec.loader)

# Subinterpreters: a module with C globals is what breaks here (3.14 stdlib).
from concurrent import interpreters
interp = interpreters.create()
interp.exec("import math; print('ok in subinterpreter', math.pi)")

Then take the skeleton above and, on paper, add a second module-level function that needs the Error type, plus a method on Counter that also needs it. Write out which mechanism each one uses to reach module state, and why they differ.

Further reading