Skip to main content

Include/moduleobject.h + Include/cpython/moduleobject.h — module object internals

Module creation in CPython has two paths: the legacy single-phase path (PyModule_Create) and the multi-phase path (PyModuleDef_Init plus slot callbacks). Both paths share PyModuleDef as the central descriptor. The public header (Include/moduleobject.h) exposes the stable ABI surface, while the cpython-tier header adds internal fields and the slot constants used for multi-phase initialization.

Map

LinesSymbolKindNotes
1-12guard + includesboilerplatepublic header only
14-22PyModuleDef_Basestructref-count + linked-list node for all loaded defs
24-55PyModuleDefstructfull descriptor, see Reading below
57-70PyModule_NewC APIcreates module by name, no def
71-80PyModule_NewObjectC APIsame but accepts PyObject *name
82-95PyModule_Create / PyModule_Create2C APIsingle-phase init from def
97-110PyModuleDef_InitC APIregisters def for multi-phase init
112-124PyModuleDef_Slotstructslot index + value pointer
126-134Py_mod_create / Py_mod_exec#defineslot indices 1 and 2
136-142Py_mod_multiple_interpreters#defineslot index 3, 3.12+
144-150PyModule_GetDict / PyModule_GetNameC APIdict and name accessors

Reading

PyModuleDef_Base and PyModuleDef

PyModuleDef_Base is a small header prepended to every PyModuleDef so the interpreter can keep a linked list of all module definitions across imports:

typedef struct PyModuleDef_Base {
PyObject_HEAD
PyObject *(*m_init)(void);
Py_ssize_t m_index;
PyObject *m_copy;
} PyModuleDef_Base;

The full PyModuleDef adds the user-facing fields:

typedef struct PyModuleDef {
PyModuleDef_Base m_base;
const char *m_name;
const char *m_doc;
Py_ssize_t m_size; /* -1 = no per-interpreter state */
PyMethodDef *m_methods;
PyModuleDef_Slot *m_slots; /* NULL for single-phase init */
traverseproc m_traverse;
inquiry m_clear;
freefunc m_free;
} PyModuleDef;

m_size == -1 means the module cannot be safely used across sub-interpreters (its state lives in C globals). A non-negative m_size allocates that many bytes of per-interpreter module state accessible via PyModule_GetState.

Multi-phase initialization slots

When m_slots is non-NULL, PyModuleDef_Init is called instead of PyModule_Create. The slots array is terminated by {0, NULL} and may contain:

#define Py_mod_create 1
#define Py_mod_exec 2
#define Py_mod_multiple_interpreters 3

typedef struct {
int slot;
void *value;
} PyModuleDef_Slot;

Py_mod_create lets the extension supply its own PyObject * (useful for subclassing ModuleType). Py_mod_exec is a callback typed as int (*)(PyObject *module) that populates the module after creation; any non-zero return triggers an import error. Py_mod_multiple_interpreters was added in 3.12 to opt a module into safe sub-interpreter use.

PyModule_New and PyModule_GetDict

PyObject *PyModule_New(const char *name);
PyObject *PyModule_GetDict(PyObject *module); /* borrowed ref */

PyModule_New is the minimal constructor: it creates a bare module with an empty __dict__ and sets only __name__. PyModule_GetDict returns the module's underlying dict as a borrowed reference; callers must not decref it.

gopy notes

  • objects/module.go defines Module with fields mirroring PyModuleDef: Name, Doc, Methods, and a State slice for m_size > 0 cases.
  • Single-phase init is handled by NewModule in objects/module.go; multi-phase init (Py_mod_exec callback) is driven from vm/eval_import.go during the import machinery.
  • Py_mod_multiple_interpreters is not yet enforced in gopy because sub-interpreter isolation is a future milestone; the slot is parsed and stored but not acted on.
  • PyModule_GetDict maps to m.Dict() returning *Dict without incrementing the reference count, preserving the borrowed-ref contract.