Goal
This article explains, through an example, how to structure a Python C extension module using multi-phase initialization.
In Python, there are two ways to create a C extension module:
- Single phase intialization
- Multi-phase intialization
This article focuses on multi-phase initialization, introduced in Python 3.5 by PEP 489.
Single-phase initialization is still supported, but multi-phase initialization is recommended for new modules.
Code Part (Step by Step)
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string.h>ℹ️️#define PY_SSIZE_T_CLEAN
PY_SSIZE_T_CLEAN macro is no longer required since Python 3.13. But might be kept, just for backward compatibility.
It was required for Python 3.12 and earlier when using argument-parsing formats that return a size, such as “s#”, “y#”, “z#”, “u#”, “es#” and “et#” (such format is used in function PyArg_ParseTupleAndKeywords() from CPython, used to retrieve arguments in C function in your module).
These formats provide both a pointer to the object data and its length. Without this macro, the length was expected as an int in C. With the macro,it had to be a Py_ssize_t.
Py_ssize_t is a signed integer type with the same size as size_t. Unlike size_t, it can represent negative values, including -1, which is commonly used by the Python/C API as an error indicator.
ℹ️️ #include
Python.h provides the functions, macros and structures needed to interact with the Python interpreter and to initialize a C extension module.
#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* 1. Module State */
/* ------------------------------------------------------------------ */
typedef struct {
long counter;
char* message;
} module_state;
/* ------------------------------------------------------------------ */
/* 2. Fonctions exposées à Python */
/* ------------------------------------------------------------------ */
static PyObject*
add(PyObject* self, PyObject* args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
return PyLong_FromLong(a + b);
}
static PyObject*
increment_counter(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
state->counter++;
return PyLong_FromLong(state->counter);
}
static PyObject*
get_counter(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
return PyLong_FromLong(state->counter);
}
static PyObject*
set_message(PyObject* self, PyObject* args)
{
const char* text;
module_state* state;
char* copy;
if (!PyArg_ParseTuple(args, "s", &text)) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
copy = PyMem_Malloc(strlen(text) + 1);
if (copy == NULL) {
PyErr_NoMemory();
return NULL;
}
strcpy(copy, text);
PyMem_Free(state->message);
state->message = copy;
Py_RETURN_NONE;
}
static PyObject*
get_message(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
if (state->message == NULL) {
Py_RETURN_NONE;
}
return PyUnicode_FromString(state->message);
}
static PyObject*
divide(PyObject* self, PyObject* args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
if (b == 0) {
PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
return NULL;
}
return PyFloat_FromDouble((double)a / (double)b);
}
/* ------------------------------------------------------------------ */
/* 3. Table des méthodes */
/* ------------------------------------------------------------------ */
static PyMethodDef mymodule_methods[] = {
{"add", add, METH_VARARGS,
"add(a, b) -> int\n\nReturn the sum of two integers."},
{"divide", divide, METH_VARARGS,
"divide(a, b) -> float\n\nReturn a divided by b."},
{"increment_counter", increment_counter, METH_VARARGS,
"increment_counter() -> int\n\nIncrement and return the module counter."},
{"get_counter", get_counter, METH_VARARGS,
"get_counter() -> int\n\nReturn the current module counter."},
{"set_message", set_message, METH_VARARGS,
"set_message(text) -> None\n\nStore a message in the module state."},
{"get_message", get_message, METH_VARARGS,
"get_message() -> str or None\n\nReturn the stored message."},
{NULL, NULL, 0, NULL}
};
/* ------------------------------------------------------------------ */
/* 4. Slot Py_mod_exec : initialisation du module */
/* ------------------------------------------------------------------ */
static int
mymodule_exec(PyObject* module)
{
module_state* state;
PyObject* my_error;
/* Récupère l’état alloué par CPython grâce à m_size. */
state = (module_state*)PyModule_GetState(module);
if (state == NULL) {
return -1;
}
/* Initialisation explicite de l’état. */
state->counter = 0;
state->message = NULL;
/* Ajout des fonctions. */
if (PyModule_AddFunctions(module, mymodule_methods) < 0) {
return -1;
}
/* Ajout d’une constante. */
if (PyModule_AddIntConstant(module, "VERSION", 1) < 0) {
return -1;
}
/* Création et ajout d’une exception personnalisée. */
my_error = PyErr_NewException("mymodule.MyError", NULL, NULL);
if (my_error == NULL) {
return -1;
}
if (PyModule_AddObjectRef(module, "MyError", my_error) < 0) {
Py_DECREF(my_error);
return -1;
}
Py_DECREF(my_error);
return 0;
}
/* ------------------------------------------------------------------ */
/* 5. Libération de l’état */
/* ------------------------------------------------------------------ */
static void
mymodule_free(void* raw_state)
{
module_state* state = (module_state*)raw_state;
if (state != NULL) {
PyMem_Free(state->message);
state->message = NULL;
}
}
/* ------------------------------------------------------------------ */
/* 6. Définition multi-phase du module */
/* ------------------------------------------------------------------ */
static struct PyModuleDef mymodule_module = {
PyModuleDef_HEAD_INIT,
.m_name = "mymodule",
.m_doc = "Exemple complet de module Python en C avec initialisation multi-phase.",
.m_size = sizeof(module_state),
.m_methods = mymodule_methods,
.m_slots = (PyModuleDef_Slot[]) {
{Py_mod_exec, mymodule_exec},
{0, NULL}
},
.m_free = mymodule_free
};
/* ------------------------------------------------------------------ */
/* 7. Point d’entrée obligatoire */
/* ------------------------------------------------------------------ */
PyMODINIT_FUNC
PyInit_mymodule(void)
{
return PyModuleDef_Init(&mymodule_module);
}</kbd>#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* 1. État du module */
/* ------------------------------------------------------------------ */
typedef struct {
long counter;
char* message;
} module_state;
/* ------------------------------------------------------------------ */
/* 2. Fonctions exposées à Python */
/* ------------------------------------------------------------------ */
static PyObject*
add(PyObject* self, PyObject* args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
return PyLong_FromLong(a + b);
}
static PyObject*
increment_counter(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
state->counter++;
return PyLong_FromLong(state->counter);
}
static PyObject*
get_counter(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
return PyLong_FromLong(state->counter);
}
static PyObject*
set_message(PyObject* self, PyObject* args)
{
const char* text;
module_state* state;
char* copy;
if (!PyArg_ParseTuple(args, "s", &text)) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
copy = PyMem_Malloc(strlen(text) + 1);
if (copy == NULL) {
PyErr_NoMemory();
return NULL;
}
strcpy(copy, text);
PyMem_Free(state->message);
state->message = copy;
Py_RETURN_NONE;
}
static PyObject*
get_message(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
if (state->message == NULL) {
Py_RETURN_NONE;
}
return PyUnicode_FromString(state->message);
}
static PyObject*
divide(PyObject* self, PyObject* args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
if (b == 0) {
PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
return NULL;
}
return PyFloat_FromDouble((double)a / (double)b);
}
/* ------------------------------------------------------------------ */
/* 3. Table des méthodes */
/* ------------------------------------------------------------------ */
static PyMethodDef mymodule_methods[] = {
{"add", add, METH_VARARGS,
"add(a, b) -> int\n\nReturn the sum of two integers."},
{"divide", divide, METH_VARARGS,
"divide(a, b) -> float\n\nReturn a divided by b."},
{"increment_counter", increment_counter, METH_VARARGS,
"increment_counter() -> int\n\nIncrement and return the module counter."},
{"get_counter", get_counter, METH_VARARGS,
"get_counter() -> int\n\nReturn the current module counter."},
{"set_message", set_message, METH_VARARGS,
"set_message(text) -> None\n\nStore a message in the module state."},
{"get_message", get_message, METH_VARARGS,
"get_message() -> str or None\n\nReturn the stored message."},
{NULL, NULL, 0, NULL}
};
/* ------------------------------------------------------------------ */
/* 4. Slot Py_mod_exec : initialisation du module */
/* ------------------------------------------------------------------ */
static int
mymodule_exec(PyObject* module)
{
module_state* state;
PyObject* my_error;
/* Récupère l’état alloué par CPython grâce à m_size. */
state = (module_state*)PyModule_GetState(module);
if (state == NULL) {
return -1;
}
/* Initialisation explicite de l’état. */
state->counter = 0;
state->message = NULL;
/* Ajout des fonctions. */
if (PyModule_AddFunctions(module, mymodule_methods) < 0) {
return -1;
}
/* Ajout d’une constante. */
if (PyModule_AddIntConstant(module, "VERSION", 1) < 0) {
return -1;
}
/* Création et ajout d’une exception personnalisée. */
my_error = PyErr_NewException("mymodule.MyError", NULL, NULL);
if (my_error == NULL) {
return -1;
}
if (PyModule_AddObjectRef(module, "MyError", my_error) < 0) {
Py_DECREF(my_error);
return -1;
}
Py_DECREF(my_error);
return 0;
}
/* ------------------------------------------------------------------ */
/* 5. Libération de l’état */
/* ------------------------------------------------------------------ */
static void
mymodule_free(void* raw_state)
{
module_state* state = (module_state*)raw_state;
if (state != NULL) {
PyMem_Free(state->message);
state->message = NULL;
}
}
/* ------------------------------------------------------------------ */
/* 6. Définition multi-phase du module */
/* ------------------------------------------------------------------ */
static struct PyModuleDef mymodule_module = {
PyModuleDef_HEAD_INIT,
.m_name = "mymodule",
.m_doc = "Exemple complet de module Python en C avec initialisation multi-phase.",
.m_size = sizeof(module_state),
.m_methods = mymodule_methods,
.m_slots = (PyModuleDef_Slot[]) {
{Py_mod_exec, mymodule_exec},
{0, NULL}
},
.m_free = mymodule_free
};
/* ------------------------------------------------------------------ */
/* 7. Point d’entrée obligatoire */
/* ------------------------------------------------------------------ */
PyMODINIT_FUNC
PyInit_mymodule(void)
{
return PyModuleDef_Init(&mymodule_module);
}</kbd>Start with source code.
There is 2 method : single phase and multiphase.
The single phase method is not anymore recommended (is it obsolete ? Not sure yet) ==> Why not to build a program to migrate source code to single phase to multiphase . Maybe it is possible ? but it is another subject.
Focus ont the source code : here is a generic template . Let start by practice the code to understand what to do.
Et dans le cœur de Python :
Include/pyport.hcontient le commentaire : “Now PY_SSIZE_T_CLEAN is mandatory. This is just for backward compatibility.”- et il définit aussi :
typedef Py_ssize_t Py_ssize_clean_t;
Donc la règle pratique est :
- avant Python 3.13 : le définir avant
#include <Python.h>pour utiliser les APIs avec# - à partir de Python 3.13 : ce n’est plus strictement nécessaire, mais on le garde pour compatibilité
En bref, PY_SSIZE_T_CLEAN sert à garantir le bon type Py_ssize_t dans les bindings C/extension, et le dépôt le montre surtout dans la documentation et les exemples, pas dans le cœur du header principal.#ifdef PY_SSIZE_T_CLEAN is a compile-time guard used by CPython’s C API to switch some functions to the Py_ssize_t-based variants.
In practice, the common pattern is:
C
#define PY_SSIZE_T_CLEAN
#include <Python.h>
Then APIs that take a length/count or a buffer size use Py_ssize_t instead of int for certain format units such as:
s#y#z#- and similar
#-based argument parsing formats
Example:
C
char *buf;
Py_ssize_t len;
PyArg_ParseTuple(args, "s#", &buf, &len);
Without PY_SSIZE_T_CLEAN, some older Python versions could treat the length as int instead of Py_ssize_t, which is not ideal for large sizes and is a source of ABI/API mismatch problems.
Important CPython notes:
- On Python 3.12 and older, this macro is required to use the
#variants safely. - On Python 3.13 and later, it is no longer necessary in most cases.
- In newer CPython code, this is considered a compatibility macro: the runtime still supports it, but modern code mostly uses the direct
Py_ssize_t-aware APIs without needing it.
You can also see this in CPython docs:
Doc/c-api/arg.rstsays it must be defined on Python 3.12 and older for all#format variants.Include/pyport.hsays this is now effectively kept only for backward compatibility.
So in short:
PY_SSIZE_T_CLEAN= “usePy_ssize_tfor size/count-like C API arguments”- It is mainly relevant to argument parsing and buffer APIs
- It is not a runtime flag, just a macro that changes the C API contract at compile time
If you want, I can also show exactly which C API functions in CPython are affected and how to use Py_ssize_t correctly in an extension module.Py_ssize_t n’est pas défini en fonction de PY_SSIZE_T_CLEAN. Il est défini dans Include/pyport.h. La macro PY_SSIZE_T_CLEAN change plutôt quelle fonction de parsing est utilisée, et donc le type attendu pour les longueurs des formats # comme s# ou y#.huggingface
Définition de Py_ssize_t
Dans Include/pyport.h :
ctypedef ssize_t Py_ssize_t;
lorsque la plateforme fournit ssize_t. Sinon, CPython choisit un type entier signé ayant la même taille que size_t :
csizeof(Py_ssize_t) == sizeof(size_t)
Donc :
textPy_ssize_t : toujours défini dans Include/pyport.h
PY_SSIZE_T_CLEAN : contrôle le comportement des formats "#"
Où agit PY_SSIZE_T_CLEAN
La macro est utilisée dans Include/modsupport.h. Elle redirige les fonctions de parsing vers leurs variantes _SizeT :
c#ifdef PY_SSIZE_T_CLEAN
#define PyArg_Parse _PyArg_Parse_SizeT
#define PyArg_ParseTuple _PyArg_ParseTuple_SizeT
#define PyArg_ParseTupleAndKeywords _PyArg_ParseTupleAndKeywords_SizeT
#define Py_VaBuildValue _Py_VaBuildValue_SizeT
#endif
Donc, avec :
c#define PY_SSIZE_T_CLEAN
#include <Python.h>
ce code :
cPyArg_ParseTuple(args, "y#", &data, &length);
appelle en réalité :
c_PyArg_ParseTuple_SizeT(args, "y#", &data, &length);
Effet sur les formats #
| Macro définie ? | Fonction appelée | Type attendu pour s#, y#, etc. |
|---|---|---|
| Non | PyArg_ParseTuple | int |
| Oui | _PyArg_ParseTuple_SizeT | Py_ssize_t |
huggingface+1
Exemple :
c#define PY_SSIZE_T_CLEAN
#include <Python.h>
const char *data;
Py_ssize_t length;
PyArg_ParseTuple(args, "y#", &data, &length);
Ici, length doit être un Py_ssize_t.cmi.ac+1
Où est implémenté le comportement
L’implémentation est dans :
textPython/getargs.c
notamment dans les fonctions de conversion comme convertsimple(). C’est là que CPython vérifie, pour les anciennes versions, que la macro a bien été définie avant d’accepter un format # :
c#define REQUIRE_PY_SSIZE_T_CLEAN \
if (!(flags & FLAG_SIZE_T)) { \
PyErr_SetString(PyExc_SystemError, \
"PY_SSIZE_T_CLEAN macro must be defined for '#' formats"); \
return NULL; \
}
Résumé des fichiers
| Fichier CPython | Rôle |
|---|---|
Include/pyport.h | Définit Py_ssize_t, PY_SSIZE_T_MAX, PY_SSIZE_T_MIN |
Include/Python.h | Inclut pyport.h, modsupport.h, etc. |
Include/modsupport.h | Utilise PY_SSIZE_T_CLEAN pour rediriger vers les variantes _SizeT |
Python/getargs.c | Implémente le parsing et le traitement des formats # |
Python/modsupport.c | Implémente notamment Py_BuildValue() |
huggingface+1
Depuis Python 3.13
Depuis Python 3.13, PY_SSIZE_T_CLEAN n’est plus nécessaire pour les formats # : Py_ssize_t est désormais le comportement standard. La macro reste recommandée dans le code source pour compatibilité avec les versions antérieures.github
En une phrase
Py_ssize_t est défini dans Include/pyport.h indépendamment de PY_SSIZE_T_CLEAN ; cette macro, traitée dans Include/modsupport.h et Python/getargs.c, sélectionne les variantes _SizeT de PyArg_Parse*() afin que les formats # attendent un Py_ssize_t plutôt qu’un int.
#define PY_SSIZE_T_CLEAN#define PY_SSIZE_T_CLEAN
#include <Python.h>
#include <string.h>
/* ------------------------------------------------------------------ */
/* 1. État du module */
/* ------------------------------------------------------------------ */
typedef struct {
long counter;
char* message;
} module_state;
/* ------------------------------------------------------------------ */
/* 2. Fonctions exposées à Python */
/* ------------------------------------------------------------------ */
static PyObject*
add(PyObject* self, PyObject* args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
return PyLong_FromLong(a + b);
}
static PyObject*
increment_counter(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
state->counter++;
return PyLong_FromLong(state->counter);
}
static PyObject*
get_counter(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
return PyLong_FromLong(state->counter);
}
static PyObject*
set_message(PyObject* self, PyObject* args)
{
const char* text;
module_state* state;
char* copy;
if (!PyArg_ParseTuple(args, "s", &text)) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
copy = PyMem_Malloc(strlen(text) + 1);
if (copy == NULL) {
PyErr_NoMemory();
return NULL;
}
strcpy(copy, text);
PyMem_Free(state->message);
state->message = copy;
Py_RETURN_NONE;
}
static PyObject*
get_message(PyObject* self, PyObject* args)
{
module_state* state;
if (!PyArg_ParseTuple(args, "")) {
return NULL;
}
state = (module_state*)PyModule_GetState(self);
if (state == NULL) {
return NULL;
}
if (state->message == NULL) {
Py_RETURN_NONE;
}
return PyUnicode_FromString(state->message);
}
static PyObject*
divide(PyObject* self, PyObject* args)
{
long a, b;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL;
}
if (b == 0) {
PyErr_SetString(PyExc_ZeroDivisionError, "division by zero");
return NULL;
}
return PyFloat_FromDouble((double)a / (double)b);
}
/* ------------------------------------------------------------------ */
/* 3. Table des méthodes */
/* ------------------------------------------------------------------ */
static PyMethodDef mymodule_methods[] = {
{"add", add, METH_VARARGS,
"add(a, b) -> int\n\nReturn the sum of two integers."},
{"divide", divide, METH_VARARGS,
"divide(a, b) -> float\n\nReturn a divided by b."},
{"increment_counter", increment_counter, METH_VARARGS,
"increment_counter() -> int\n\nIncrement and return the module counter."},
{"get_counter", get_counter, METH_VARARGS,
"get_counter() -> int\n\nReturn the current module counter."},
{"set_message", set_message, METH_VARARGS,
"set_message(text) -> None\n\nStore a message in the module state."},
{"get_message", get_message, METH_VARARGS,
"get_message() -> str or None\n\nReturn the stored message."},
{NULL, NULL, 0, NULL}
};
/* ------------------------------------------------------------------ */
/* 4. Slot Py_mod_exec : initialisation du module */
/* ------------------------------------------------------------------ */
static int
mymodule_exec(PyObject* module)
{
module_state* state;
PyObject* my_error;
/* Récupère l’état alloué par CPython grâce à m_size. */
state = (module_state*)PyModule_GetState(module);
if (state == NULL) {
return -1;
}
/* Initialisation explicite de l’état. */
state->counter = 0;
state->message = NULL;
/* Ajout des fonctions. */
if (PyModule_AddFunctions(module, mymodule_methods) < 0) {
return -1;
}
/* Ajout d’une constante. */
if (PyModule_AddIntConstant(module, "VERSION", 1) < 0) {
return -1;
}
/* Création et ajout d’une exception personnalisée. */
my_error = PyErr_NewException("mymodule.MyError", NULL, NULL);
if (my_error == NULL) {
return -1;
}
if (PyModule_AddObjectRef(module, "MyError", my_error) < 0) {
Py_DECREF(my_error);
return -1;
}
Py_DECREF(my_error);
return 0;
}
/* ------------------------------------------------------------------ */
/* 5. Libération de l’état */
/* ------------------------------------------------------------------ */
static void
mymodule_free(void* raw_state)
{
module_state* state = (module_state*)raw_state;
if (state != NULL) {
PyMem_Free(state->message);
state->message = NULL;
}
}
/* ------------------------------------------------------------------ */
/* 6. Définition multi-phase du module */
/* ------------------------------------------------------------------ */
static struct PyModuleDef mymodule_module = {
PyModuleDef_HEAD_INIT,
.m_name = "mymodule",
.m_doc = "Exemple complet de module Python en C avec initialisation multi-phase.",
.m_size = sizeof(module_state),
.m_methods = mymodule_methods,
.m_slots = (PyModuleDef_Slot[]) {
{Py_mod_exec, mymodule_exec},
{0, NULL}
},
.m_free = mymodule_free
};
/* ------------------------------------------------------------------ */
/* 7. Point d’entrée obligatoire */
/* ------------------------------------------------------------------ */
PyMODINIT_FUNC
PyInit_mymodule(void)
{
return PyModuleDef_Init(&mymodule_module);
}</kbd>
External links to go further