Umstieg auf das stabile ABI für freies Threading (abi3t)

Seit der Veröffentlichung von 3.15 unterstützt CPython eine Variante des stabilen ABI, die free-threaded Python unterstützt: das stabile ABI für Free-Threaded-Builds, kurz abi3t. Dieses Dokument beschreibt, wie du C-API-Erweiterungen anpasst, damit sie freies Threading unterstützen.

Warum man das tun sollte

Der übliche Grund, das stabile ABI zu verwenden, ist, die Zahl der Artefakte zu verringern, die du für jede Version deiner Bibliothek bauen und verteilen musst.

Ohne das stabile ABI musst du für jede Funktionsversion von CPython, die du unterstützen willst, eine eigene dynamische Bibliothek und üblicherweise eine Wheel-Distribution bauen. Jede Kennzeichnung in der folgenden Tabelle steht zum Beispiel für eine eigene Bibliothek beziehungsweise ein eigenes Wheel:

CPython-Version

Ohne freies Threading

Mit freiem Threading

3.12

cpython-312

—

3.13

cpython-313

cpython-313t

3.14

cpython-314

cpython-314t

3.15

cpython-315

cpython-315t

3.16

cpython-316

cpython-316t

Spätere Versionen

cpython-3XX

cpython-3XXt

Das sind eine Menge Builds, besonders wenn man sie mit der Zahl der unterstützten Plattformen multipliziert.

Mit dem stabilen ABI (abi3, eingeführt in CPython 3.2) kann eine einzige Erweiterung (je Plattform) alle Builds von CPython ohne freies Threading abdecken:

CPython-Version

Ohne freies Threading

Mit freiem Threading

3.12

abi3

—

3.13

cpython-313t

3.14

cpython-314t

3.15

cpython-315t

3.16

cpython-316t

Spätere Versionen

cpython-3XXt

Das stabile ABI für Free-Threaded-Builds (abi3t), eingeführt in CPython 3.15, tut dasselbe für Free-Threaded-Builds. Und es ist zugleich mit denen ohne freies Threading kompatibel:

CPython-Version

Ohne freies Threading

Mit freiem Threading

3.12

abi3 *

—

3.13

cpython-313t

3.14

cpython-314t

3.15

abi3t

3.16

Spätere Versionen

* (Wie oben ist die abi3-Erweiterung mit allen Builds ohne freies Threading kompatibel – auch mit denen ab 3.15, die diese Tabelle abi3t „zuschreibt“.)

Warum man das nicht tun sollte

Das stabile ABI hat zwei wesentliche Nachteile.

Erstens kann deine Erweiterung langsamer werden, weil das stabile ABI die Kompatibilität über die Leistung stellt. Der Unterschied ist üblicherweise nicht bemerkbar und lässt sich oft abmildern, indem du aus derselben Quelle sowohl einen Build mit stabilem ABI als auch einige versionsspezifische Builds für die CPython-Versionen der „Stufe 1“ erzeugst.

Zweitens ist nicht die gesamte C-API verfügbar. Erweiterungen müssen portiert werden, um für das stabile ABI gebaut werden zu können, und das kann schwierig oder in seltenen Fällen unmöglich sein.

Insbesondere setzt abi3t APIs voraus, die in CPython 3.15 ergänzt wurden. Wenn du deine Erweiterung aus derselben Quelle auch für ältere CPython-Versionen bauen willst, hast du zwei wesentliche Möglichkeiten:

  • Verwende Bedingungen des Präprozessors.

    Verwende beim Befolgen dieser Anleitung #ifdef Py_TARGET_ABI3T-Blöcke, wann immer du eine Änderung vornehmen sollst, die den Build auf CPython-Versionen bricht, die dir wichtig sind. Behalte den bisherigen Code in #else-Blöcken.

    Für handgeschriebene C-Erweiterungen ist dieser Ansatz dank der Ergänzungen aus PEP 697 bis hinunter zu CPython 3.12 vernünftig. Die Kompatibilität mit 3.11 und älter aufrechtzuerhalten, kann sich für Codegeneratoren lohnen (zum Beispiel für Cython).

  • Portiere nicht auf abi3t und baue weiterhin eigene Erweiterungen für jede CPython-Version, bis du die Unterstützung für die älteren Versionen aufgeben kannst.

    Das ist ein gültiger Ansatz. Nicht jede Erweiterung muss jetzt schon auf abi3t umsteigen.

Voraussetzungen

Diese Anleitung geht davon aus, dass du eine unmittelbar in C (oder C++) geschriebene Erweiterung hast, die du auf abi3t portieren willst.

Wenn deine Erweiterung einen Codegenerator (wie Cython) oder eine Sprachanbindung (wie PyO3) verwendet, ist es am besten, zu warten, bis dieses Werkzeug abi3t unterstützt. Wenn du ein solches Werkzeug pflegst, kannst du die Hinweise hier möglicherweise für dein Werkzeug anpassen.

Stabiles ABI ohne freies Threading

Deine Erweiterung sollte das stabile ABI ohne freies Threading (abi3) unterstützen. Falls nicht, portiere sie entweder zuerst darauf oder folge dieser Anleitung und sei darauf vorbereitet, Probleme zu beheben, die sie nicht erwähnt.

Unterstützung für freies Threading

Es ist technisch gesehen keine harte Voraussetzung, aber du wirst deine Erweiterung höchstwahrscheinlich auf freies Threading vorbereiten wollen, bevor du sie auf abi3t portierst. Hinweise dazu stehen unter Unterstützung von freiem Threading in C-API-Erweiterungen.

Siehe auch

Porting Extension Modules to Support Free-Threading: eine von der Gemeinschaft gepflegte Portierungsanleitung für Autoren von Erweiterungen.

Erweiterungsmodule isolieren

Dein Modul sollte eine mehrphasige Initialisierung verwenden und entweder isoliert sein oder sich darauf beschränken, je Prozess höchstens einmal geladen zu werden. Ist das bei dir nicht der Fall, folge zuerst Erweiterungsmodule isolieren. (Eine Abkürzung findest du im Abschnitt zum Verzicht.)

Typen mit veränderlicher Größe vermeiden

Wenn deine Erweiterung Typen mit veränderlicher Größe definiert (über Py_tp_itemsize oder PyTypeObject.tp_itemsize), lässt sie sich nicht auf abi3t 3.15 portieren.

Den Build einrichten

If you use a build tool, search its documentation for „abi3t“, and follow any instructions to select the ABI. For reference, here are direct links for several popular build tools:

You may want to verify that the tool set the right flag by temporarily adding the following just after #include <Python.h>:

#if Py_TARGET_ABI3T+0 <= 0x30f0000
#error "abi3t define is not set!"
#endif

Das sollte zu einem anderen Fehler führen als „abi3t define is not set“.

Siehe auch

Building and distributing abi3t extensions: Build configuration and wheel testing examples in the community-maintained Python Free-Threading Guide.

Bemerkung

Wenn dein Build-Werkzeug abi3t noch nicht unterstützt, setze vor dem Einbinden von Python.h das folgende Makro:

#define Py_TARGET_ABI3T 0x30f0000

oder gib es als Compilerflag an, zum Beispiel:

-DPy_TARGET_ABI3T=0x30f0000

Sobald deine Erweiterung mit dieser Einstellung baut, ist sie mit CPython 3.15 und neuer kompatibel.

Wenn du dieses Makro von Hand setzt, musst du die entstehende Erweiterung später ebenfalls von Hand benennen und kennzeichnen. Das wird unten unter Kennzeichnen und Verteilen behandelt.

Diese Anleitung bittet dich, eine Reihe von Änderungen vorzunehmen. Prüfe nach jeder einzelnen, ob deine Erweiterung in der ursprünglichen Konfiguration (ohne abi3t) noch baut, und lasse im Idealfall die Tests auf allen Python-Versionen laufen, die du unterstützt. So stellst du sicher, dass beim Portieren nichts bricht.

Export-Haken des Moduls

Unless you’ve done this step already, your extension module defines a module initialization function named PyInit_<modname> (where modname is the name of your module). You will need to port it to a module export hook, PyModExport_<modname>, a feature added in CPython 3.15 in PEP 793.

Deine bestehende init-Funktion sollte so aussehen (mit deinen eigenen Namen für <modname> und <moddef>):

PyMODINIT_FUNC
PyInit_<modname>(void)
{
    return PyModuleDef_Init(&<moddef>);
}

Steht vor dem return noch Code, verschiebe ihn in eine Slot-Funktion Py_mod_create oder Py_mod_exec. Verwandte Informationen stehen in der PyInit-Dokumentation.

Die Funktion verweist auf ein PyModuleDef-Objekt (<moddef> im obigen Code). Dessen Definition dürfte dem Folgenden ähneln, mit anderen Werten und vielleicht einigen unbenannten oder weggelassenen Feldern:

static PyModuleDef <moddef> = {
    PyModuleDef_HEAD_INIT,
    .m_name = "my_module",
    .m_doc = "my docstring",
    .m_size = sizeof(my_state_struct),
    .m_methods = my_methods,
    .m_slots = my_slots,
    .m_traverse = my_traverse,
    .m_clear = my_clear,
    .m_free = my_free,
};

Entferne diese Definition und die PyInit-Funktion (oder stecke sie in einen #ifndef Py_TARGET_ABI3T-Block, um die Rückwärtskompatibilität zu erhalten) und ersetze sie durch Folgendes:

PyABIInfo_VAR(abi_info);

static PySlot my_slot_array[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "my_module"),
    PySlot_STATIC_DATA(Py_mod_doc, "my docstring"),
    PySlot_SIZE(Py_mod_state_size, sizeof(my_state_struct)),
    PySlot_STATIC_DATA(Py_mod_methods, my_methods),
    PySlot_STATIC_DATA(Py_mod_slots, my_slots),
    PySlot_FUNC(Py_mod_state_traverse, my_traverse),
    PySlot_FUNC(Py_mod_state_clear, my_clear),
    PySlot_FUNC(Py_mod_state_free, my_free),
    PySlot_END
};

PyMODEXPORT_FUNC
PyModExport_<modname>(void)
{
    return my_slot_array;
}

Lasse alle Felder weg, die gefehlt haben (außer dem neuen Py_mod_abi), und setze deine eigenen Werte ein.

Einzelheiten zu dieser API stehen in der Dokumentation zu PySlot und zum Export-Haken.

Wie im Beispiel sollte deine PyModExport_-Funktion nur einen Zeiger auf statische Daten zurückgeben. Wenn du zusätzlichen Code nicht vermeiden kannst, lies die Vorbehalte in der PyModExport-Dokumentation.

Bemerkung

When building for Windows using the Setuptools build tool, removing the PyInit_<modname> function may result in the linker error LINK : error LNK2001: unresolved external symbol PyInit_<modname>. This is caused by Setuptools passing an /EXPORT linker flag, which is redundant since Python 3.15 (see gh-141671). A workaround is to add a dummy PyInit_<modname> function to your code. Python 3.15+ will never call this function if PyModExport_<modname> is present, so it can always fail:

// Workaround for https://github.com/pypa/distutils/issues/387
PyMODINIT_FUNC
PyInit_<modname>(void)
{
    PyErr_SetString(PyExc_SystemError,
                    "PyInit_* called for module with PyModExport_*");
    return NULL;
}

(This issue is present in Setuptools 84.0.0; it might be fixed in newer versions.)

Bestehende Slots

Wenn du einen Py_mod_slots-Slot hast, prüfe das Feld, auf das er verweist. Es sollte ein PyModuleDef_Slot-Feld wie das folgende sein:

static PyObject *create_module(PyObject *spec, PyModuleDef *def) { ... }
static int my_first_module_exec(PyObject *module) { ... }
static int my_second_module_exec(PyObject *module) { ... }

static PyModuleDef_Slot my_slots[] = {
   {Py_mod_gil, Py_MOD_GIL_NOT_USED},
   {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
   {Py_mod_create, my_module_create},
   {Py_mod_exec, my_first_module_exec},
   {Py_mod_exec, my_second_module_exec},
   {0, NULL}
};

py_mod_create

Wenn du einen Py_mod_create-Eintrag hast, stelle sicher, dass die Funktion mit NULL als zweitem Argument aufgerufen werden kann (anstelle des PyModuleDef, das du entfernst). Oft wird dieses Argument überhaupt nicht verwendet. Du kannst das prüfen, indem du es umbenennst:

static PyObject *create_module(PyObject *spec, PyModuleDef *_unused) { ... }

Wird das Argument verwendet, finde einen anderen Weg, die Daten hineinzugeben. Üblicherweise ist die Information statisch und du kannst unmittelbar darauf verweisen. (Wenn du eine einzige Funktion für mehrere verschiedene Module wiederverwendest, erwäge, stattdessen mehrere Funktionen zu definieren.)

Multiple py_mod_exec

Wenn du mehr als einen Py_mod_exec-Eintrag hast, fasse sie zusammen: Erzeuge eine neue Funktion, die die anderen aufruft, und ersetze die bestehenden Slots durch sie.

static int my_module_exec(PyObject *module) {
   if (my_first_module_exec(module) < 0) return -1;
   if (my_second_module_exec(module) < 0) return -1;
}

static PyModuleDef_Slot my_slots[] = {
   ...
   /* (remove other Py_mod_exec slots) */
   ...
   {Py_mod_exec, my_module_exec},
   {0, NULL}
};

Werden die Funktionen nicht anderswo verwendet, kannst du ihre Rümpfe stattdessen zusammenlegen.

Slot-Felder zusammenführen

Wenn du die Kompatibilität mit Python 3.14 aufgibst, kannst du den Code auf Wunsch aufräumen, indem du die Slots in das PySlot-Feld verschiebst und die Definitionen auf PySlot_DATA und PySlot_FUNC umstellst:

static PySlot my_slot_array[] = {
    ...
    PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
    PySlot_DATA(Py_mod_multiple_interpreters,
         Py_MOD_PER_INTERPRETER_GIL_SUPPORTED)
    PySlot_FUNC(Py_mod_create, my_module_create),
    PySlot_FUNC(Py_mod_exec, my_module_exec),
    PySlot_END
};

Wenn du das tust, lösche das ursprüngliche PyModuleDef_Slot-Feld und seinen Py_mod_slots-Eintrag.

Zugehöriges PyModuleDef

Da die neue API keine PyModuleDef-Struktur verwendet, wird dem entstehenden Modul keine Definition zugeordnet. Das ändert das Verhalten der folgenden Funktionen:

Prüfe deinen Code auf diese Funktionen. Wenn du sie nicht verwendest, kannst du diesen Abschnitt überspringen.

Diese Funktionen werden üblicherweise für zwei Zwecke verwendet:

  1. Um die Definition zu bekommen, mit der das Modul erzeugt wurde. Mit der neuen API ist das nicht mehr möglich. Module halten keine Referenz mehr auf die Definition. Du musst dir also einen anderen Weg überlegen, die relevanten Daten weiterzugeben.

  1. Um zu prüfen, ob ein gegebenes Modulobjekt „deines“ ist. Dieser Anwendungsfall wird jetzt über Modul-Token abgedeckt – undurchsichtige Zeiger, die ein Modul kennzeichnen. Um ein Token zu verwenden, deklariere (oder wiederverwende) eine eindeutige statische Variable, zum Beispiel:

    static char my_token;
    

    und ergänze in einem neuen Eintrag des PySlot-Feldes deines Moduls einen Zeiger darauf:

    static PySlot my_slot_array[] = {
       ...
       PySlot_STATIC_DATA(Py_mod_token, &my_token),
       PySlot_END
    }
    

    Wechsle dann von PyModule_GetDef()-Aufrufen wie:

    PyModuleDef *def = PyModule_GetDef(module);
    

    zu PyModule_GetToken() (das ein Ausgabeargument verwendet und mit einer Ausnahme scheitern kann):

    void *token;
    if (PyModule_GetToken(module, &token) < 0) {
        /* handle error */
    }
    

    und von PyType_GetModuleByDef()-Aufrufen wie:

    PyObject *module = PyType_GetModuleByDef(type, my_def);
    /* handle error; use module */
    

    zu PyType_GetModuleByToken() (das eine starke Referenz zurückgibt):

    PyObject *module = PyType_GetModuleByToken(type, my_token);
    /* handle error; use module */
    Py_XDECREF(module);
    

Undurchsichtigkeit von PyObject

Die Strukturen PyObject und PyVarObject sind in abi3t undurchsichtig.

Der Zugriff auf ihre Felder ist verboten. Wenn du das tust, wechsle zu den Lese- und Schreibfunktionen, die in deren Dokumentation genannt sind:

Außerdem ist die Größe der PyObject-Strukturen dem Compiler unbekannt. Sie kann sich zwischen verschiedenen CPython-Builds ändern – und sie tut es.

Bemerkung

Die Größe ist zur Laufzeit zwar verfügbar (in Python-Code zum Beispiel als sys.getsizeof(object())), du solltest der Versuchung aber widerstehen, daraus Zeigerversätze zu berechnen. Die Speicheranordnung der Objekte kann sich in künftigen abi3t-Implementierungen ändern.

Eigene Typdefinitionen

Da PyObject undurchsichtig ist, funktioniert der herkömmliche Weg, eigene Typen zu definieren, nicht mehr:

typedef struct {
   PyObject_HEAD  // expands to `PyObject ob_base;` which has unknown size

   int my_data;
} CustomObject;

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = sizeof(CustomObject),
   ...
};

Höchstwahrscheinlich müssen alle deine Klassendefinitionen und aller Code, der auf die Daten deiner Klassen zugreift, neu geschrieben werden. Das dürfte die größte Änderung sein, die du für abi3t brauchst.

Definiere für jeden solchen Typ, anstelle einer struct für die gesamte Instanz, eine mit ausschließlich den „zusätzlichen“ Feldern – denen, die zu deiner Klasse gehören und nicht zu ihren Oberklassen:

typedef struct {
   int my_data;
} CustomObjectData;

Ändere den Namen. Fast aller Code, der die Struktur verwendet, muss sich ändern (vor allem lassen sich Zeiger auf die neue Struktur nicht von und nach PyObject* umwandeln), und die Namensänderung hebt die Verwendungsstellen als Compilerfehler hervor. (Wenn du typeof, auto aus C++ oder ähnliche Wege nutzt, um den Typnamen nicht ausschreiben zu müssen, funktioniert das nicht. Sei dann besonders vorsichtig und erwäge, Werkzeuge zum Aufspüren undefinierten Verhaltens laufen zu lassen.)

Verwende dann zum Erzeugen der Klasse eine negative basicsize, um „zusätzlichen“ Speicherplatz anzugeben und nicht die gesamte Instanzgröße:

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = -sizeof(CustomObjectData), /* note the minus sign */
   ...
};

Wenn du Py_tp_members verwendest, setze bei jedem Feld das Flag Py_RELATIVE_OFFSET und gib den offset relativ zu deiner neuen Struktur an.

Zugriff auf die Daten eigener Typen

Dann kommt der schwierige Teil: In allem Code, der auf diese Struktur zugreifen muss, brauchst du einen zusätzlichen Aufruf von PyObject_GetTypeData(), um aus einem PyObject * einen Zeiger CustomObjectData * zu gewinnen:

PyObject *obj = ...;
CustomObjectData *data = PyObject_GetTypeData(obj, cls);

Beachte, dass dieser Aufruf das Typobjekt deiner Klasse (cls) verlangt.

Ist deine Klasse nicht ableitbar (verwendet sie also nicht das Flag Py_TPFLAGS_BASETYPE), ist cls gleich Py_TYPE(obj). Andernfalls VERWENDE Py_TYPE NICHT mit PyObject_GetTypeData(): Es könnte Speicher zurückgeben, der einer nicht verwandten Unterklasse vorbehalten ist! Wenn ein Benutzer zum Beispiel eine Unterklasse wie diese anlegt:

class Sub(YourCustomClass):
   __slots__ = ('a', 'b')

dann ist Py_TYPE(obj) gleich Sub, und der darunterliegende Speicher kann so aussehen:

╭─ PyObject *obj
│              ╭─ the pointer you want
│              │                    ╭─ PyObject_GetTypeData(obj, Py_TYPE(obj))
▼              ▼                    ▼
┌──────────┬───┬────────────────┬───┬─────────────┬───┬─────────────┐
│ PyObject │...│ CustomTypeData │...│ PyObject *a │...│ PyObject *b │
└──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘

(Auslassungspunkte deuten möglichen Füllraum an. Beachte, dass diese Speicheranordnung nicht zugesichert ist: Künftige Python-Versionen können anderen Füllraum ergänzen oder sogar die Reihenfolge der Strukturen ändern.)

Es gibt zwei wesentliche Wege, die richtige Klasse zu bekommen:

  • In Instanzmethoden kann deine Implementierung die Signatur PyCMethod verwenden (und das Bit METH_METHOD in PyMethodDef.ml_flags) und die Klasse als Argument defining_class bekommen.

  • Andernfalls gib deiner Klasse über den Slot Py_tp_token ein eindeutiges statisches Token und verwende:

    PyTypeObject cls;
    if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, &cls) < 0) {
        /* handle error */
    }
    CustomObjectData *data = PyObject_GetTypeData(obj, cls);
    

    Typ-Token arbeiten ähnlich wie die Modul-Token, die früher in dieser Anleitung behandelt wurden.

Bedingungen zur Build-Zeit vermeiden

Prüfe deinen Code auf APIs, die die Python-Version bestimmen, mit der deine Erweiterung gebaut wurde. Diese entspricht nicht mehr dem Python, auf dem deine Erweiterung läuft; Code, der diese Information nutzt, muss daher häufig geändert werden. Die Makros, auf die zu prüfen ist, sind:

  • PY_VERSION_HEX, PY_MAJOR_VERSION, PY_MINOR_VERSION:

    • um die Laufzeitversion zu bekommen, verwende Py_Version;

    • um zu bestimmen, welche C-API verfügbar ist, verwende Py_TARGET_ABI3T. Dieses Makro ist auf die mindestens unterstützte Version gesetzt.

  • Py_GIL_DISABLED: Unter abi3t ist dieses Makro immer definiert. Code, der mit Python mit freiem Threading arbeitet, sollte auch mit eingeschaltetem GIL arbeiten (weil der GIL zur Laufzeit eingeschaltet werden kann), und üblicherweise tut er das auch (sofern er nicht aus irgendeinem Grund mehr als einen angehängten Thread-Zustand gleichzeitig braucht).

Weitere Codeänderungen

Wenn dir noch Compilerfehler oder Warnungen bleiben, finde einen Weg, sie zu beheben. Diese Anleitung ist leider begrenzt und kann nicht alle möglichen Codeänderungen abdecken, die Erweiterungen brauchen können.

Wenn du ein Problem findest, auf das auch andere Autoren von Erweiterungen stoßen könnten, erwäge, für diese Anleitung ein Problem zu melden (oder einen Pull Request zu schicken).

Es ist möglich, dass dein Problem für die aktuelle Fassung von abi3t nicht behoben werden kann. In diesem Fall kann eine Meldung helfen, dass es für die nächste CPython-Version vorgezogen wird.

Kennzeichnen und Verteilen

Wenn du ein Build-Werkzeug mit abi3t-Unterstützung verwendest, ist deine Erweiterung fertig, aber du solltest vielleicht prüfen, ob sie richtig gebaut wurde.

Erweiterungen, die mit abi3t gebaut wurden, sollten die folgende Dateiendung haben:

  • Unter Windows: .pyd (wie jede andere Erweiterung);

  • Unter Linux, macOS und anderen Systemen, die die Endung .so verwenden: .abi3t.so (nicht .cpython-315t.so oder .abi3.so). Beachte, dass sowohl Builds mit als auch ohne freies Threading .abi3t.so-Erweiterungen laden;

  • Andere Systeme: Frage bei deinem Distributor nach und ergänze diese Anleitung vielleicht.

Wenn du die Erweiterung als Wheel verteilst, verwende die folgenden Kennzeichnungen:

  • Python-Kennzeichnung: cp3XX, wobei XX die kleinste Python-Version ist, für die die Erweiterung gebaut wurde. (Zum Beispiel cp315, wenn du Py_TARGET_ABI3T auf 0x30f0000 gesetzt hast. Weitere Werte stehen unter Compiling for Stable ABI.)

  • ABI-Kennzeichnung: abi3.abi3t. Das ist ein komprimierter Kennzeichnungssatz, der die Unterstützung für Builds sowohl mit als auch ohne freies Threading angibt.

Der Dateiname des Wheels kann zum Beispiel so aussehen:

myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl

Siehe auch

Platform Compatibility Tags in den Metadaten zur Paketverteilung der PyPA.

Wenn der Dateiname oder die Kennzeichnungen falsch sind, korrigiere sie.

Testen

Beachte: Wenn du eine Erweiterung baust, die mit mehreren CPython-Versionen kompatibel ist, solltest du sie immer mit jeder unterstützten Version testen (zum Beispiel 3.15, 3.16 und so weiter). Das stabile ABI sichert nur die ABI-Kompatibilität zu. Es kann auch Verhaltensänderungen geben – sowohl beabsichtigte (abgedeckt durch PEP 387) als auch Fehler.

Lasse die Tests unbedingt auf Builds von CPython sowohl mit als auch ohne freies Threading laufen.

Wenn sie durchlaufen: Gratulation! Du hast eine abi3t-Erweiterung.