]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-146636: Add Free-threaded Stable ABI migration guide (GH-150580)
authorPetr Viktorin <encukou@gmail.com>
Wed, 3 Jun 2026 12:02:36 +0000 (14:02 +0200)
committerGitHub <noreply@github.com>
Wed, 3 Jun 2026 12:02:36 +0000 (14:02 +0200)
Co-authored-by: Charlie Lin <tuug@gmx.us>
Co-authored-by: da-woods <dw-git@d-woods.co.uk>
Co-authored-by: Stan Ulbrych <stan@python.org>
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Doc/bugs.rst
Doc/howto/abi3t-migration.rst [new file with mode: 0644]
Doc/howto/index.rst
Doc/tools/extensions/c_annotations.py
Doc/whatsnew/3.15.rst

index 254a22f2622bd8e059596588e321881e4904ce67..a6ea0a72e76f9db22759e4e08e0ed1f289f86fad 100644 (file)
@@ -12,6 +12,9 @@ It can be sometimes faster to fix bugs yourself and contribute patches to
 Python as it streamlines the process and involves fewer people. Learn how to
 :ref:`contribute <contributing-to-python>`.
 
+
+.. _reporting-documentation-bugs:
+
 Documentation bugs
 ==================
 
diff --git a/Doc/howto/abi3t-migration.rst b/Doc/howto/abi3t-migration.rst
new file mode 100644 (file)
index 0000000..ed7a324
--- /dev/null
@@ -0,0 +1,614 @@
+.. highlight:: c
+
+.. _abi3t-migration-howto:
+
+******************************************************
+Migrating to Stable ABI for free threading (``abi3t``)
+******************************************************
+
+Starting with the 3.15 release, CPython supports a variant of the Stable ABI
+that supports :term:`free-threaded <free threading>` Python:
+Stable ABI for Free-Threaded Builds, or ``abi3t`` for short.
+This document describes how to adapt C API extensions to support free threading.
+
+Why do this
+===========
+
+The typical reason to use Stable ABI is to reduce the number of artifacts that
+you need to build and distribute for each version of your library.
+
+Without the Stable ABI, you must build a separate shared library, and typically
+a *wheel* distribution, for each feature version of CPython you wish
+to support.
+For example, each tag in the following table represents a separate
+library/wheel:
+
++-----------------+-----------------------+------------------------+
+| CPython version | Non-free-threaded     | Free-threaded          |
++=================+=======================+========================+
+| 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``       |
++-----------------+-----------------------+------------------------+
+| Later versions  | :samp:`cpython-3{XX}` | :samp:`cpython-3{XX}t` |
++-----------------+-----------------------+------------------------+
+
+That's a lot of builds, especially when multiplied by the number
+of supported platforms.
+
+With the Stable ABI (``abi3``, introduced in CPython 3.2), a single extension
+(per platform) can cover all *non-free-threaded* builds of CPython:
+
++-----------------+-------------------+------------------------+
+| CPython version | Non-free-threaded | Free-threaded          |
++=================+===================+========================+
+| 3.12            | ``abi3``          | ---                    |
++-----------------+                   +------------------------+
+| 3.13            |                   | ``cpython-313t``       |
++-----------------+                   +------------------------+
+| 3.14            |                   | ``cpython-314t``       |
++-----------------+                   +------------------------+
+| 3.15            |                   | ``cpython-315t``       |
++-----------------+                   +------------------------+
+| 3.16            |                   | ``cpython-316t``       |
++-----------------+                   +------------------------+
+| Later versions  |                   | :samp:`cpython-3{XX}t` |
++-----------------+-------------------+------------------------+
+
+The Stable ABI for free-threaded builds (``abi3t``), introduced in
+CPython 3.15, does the same for free-threaded builds.
+And it's compatible with non-free-threaded ones as well:
+
++-----------------+-------------------+------------------+
+| CPython version | Non-free-threaded | Free-threaded    |
++=================+===================+==================+
+| 3.12            | ``abi3`` *        | ---              |
++-----------------+                   +------------------+
+| 3.13            |                   | ``cpython-313t`` |
++-----------------+                   +------------------+
+| 3.14            |                   | ``cpython-314t`` |
++-----------------+-------------------+------------------+
+| 3.15            | ``abi3t``                            |
++-----------------+                                      +
+| 3.16            |                                      |
++-----------------+                                      +
+| Later versions  |                                      |
++-----------------+-------------------+------------------+
+
+\* (As above, the ``abi3`` extension is compatible with all non-free-threaded
+builds; even the 3.15+ ones that this table "attributes" to ``abi3t``.)
+
+Why *not* do this
+-----------------
+
+There are two main downsides to Stable ABI.
+
+First, you extension may become slower, since Stable ABI prioritizes
+compatibility over performance.
+The difference is usually not noticeable, and often can be mitigated by
+using the same source to build both a Stable ABI build and a few
+version-specific ones for "tier 1" CPython versions.
+
+Second, not all of the C API is available.
+Extensions need to be ported to build for Stable ABI, which may be difficult
+or, in rare cases, impossible.
+
+Specifically, ``abi3t`` requires APIs added in CPython 3.15.
+If you want to build your extension for older versions of CPython from the
+same source, you have two main options:
+
+- Use preprocessor conditionals.
+
+  When following this guide, use ``#ifdef Py_TARGET_ABI3T`` blocks whenever
+  you are told to do a change that breaks the build on CPython versions you
+  care about. Keep the pre-existing code in ``#else`` blocks.
+
+  For hand-written C extensions, this approach is reasonable down to
+  CPython 3.12, due to additions introduced in :pep:`697`.
+  Keeping compatibility with 3.11 and below may be worth it for code
+  generators (for example, Cython).
+
+- Do not port to ``abi3t``, and continue building separate extensions for
+  each version of CPython, until you can drop support for the older versions.
+
+  This is a valid approach. Not all extensions need to switch to ``abi3t``
+  right now.
+
+
+Prerequisites
+=============
+
+This guide assumes that you have an extension written directly in C (or C++),
+which you want to port to ``abi3t``.
+
+If your extenstion uses a code generator (like Cython) or language binding
+(like PyO3), it's best to wait until that tool has support for ``abi3t``.
+If you maintain such a tool, you might be able to adapt the instructions
+here for your tool.
+
+Non-free-threaded Stable ABI
+----------------------------
+
+Your extension should support the Stable ABI (``abi3t``).
+If not, either port it first, or follow this guide but be prepared to fix
+issues it does not mention.
+
+Free-threading support
+----------------------
+
+While it's technically not a hard prerequisite, you will most likely want to
+prepare your extension for free threading before you port it to ``abi3t``.
+See :ref:`freethreading-extensions-howto` for instructions.
+
+.. seealso::
+
+   `Porting Extension Modules to Support Free-Threading
+   <https://py-free-threading.github.io/porting/>`__:
+   A community-maintained porting guide for extension authors.
+
+Isolating extension modules
+---------------------------
+
+Your module should use :ref:`multi-phase initialization <multi-phase-initialization>`,
+and it should either be isolated or limit itself to be loaded at most once
+per process.
+If it is not your case, follow :ref:`isolating-extensions-howto` first.
+(See the :ref:`opt-out section <isolating-extensions-optout>` for a shortcut.)
+
+Avoiding variable-sized types
+-----------------------------
+
+If your extension defines variable-sized types (using :c:macro:`Py_tp_itemsize`
+or :c:member:`PyTypeObject.tp_itemsize`), it cannot be ported to
+``abi3t`` 3.15.
+
+
+Setting up the build
+====================
+
+If you use a build tool (such as setuptools, meson-python, scikit-build-core),
+search its documentation for a way to select ``abi3t``.
+At the time of writing, not all of them have this; but if your tool does,
+use it.
+You may want to verify that it 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
+
+This should result in a different error than "``abt3t`` define is not set".
+
+.. note::
+
+   If your build tool doesn't support ``abi3t`` yet, set the following macro
+   before including ``Python.h``::
+
+      #define Py_TARGET_ABI3T 0x30f0000
+
+   or specify it as a compiler flag, for example::
+
+      -DPy_TARGET_ABI3T=0x30f0000
+
+   Once your extension builds with this setting, it will be compatible with
+   CPython 3.15 and above.
+
+   If you set this macro manually, you will later need to name and tag the
+   resulting extension manually as well.
+   This is covered in :ref:`abi3t-migration-tagging` below.
+
+This guide will ask you to make a series of changes.
+After each one, verify that your extension still builds in the original
+(non-``abi3t``) configuration, and ideally run tests on all Python
+versions you support.
+This will ensure that nothing breaks as you are porting.
+
+
+Module export hook
+==================
+
+Unless you've done this step already, your extension module defines a
+:ref:`module initialization function <extension-pyinit>`
+named :samp:`PyInit_{<module_name>}`.
+You will need to port it to a :ref:`module export hook <extension-export-hook>`,
+:samp:`PyModExport_{<module name>}`, a feature added in CPython 3.15 in
+:pep:`793`.
+
+Your existing init function should look like this (with your own names
+for ``<modname>`` and ``<moddef>``):
+
+.. code-block::
+   :class: bad
+
+   PyMODINIT_FUNC
+   PyInit_<modname>(void)
+   {
+       return PyModuleDef_Init(&<moddef>);
+   }
+
+If there is some code before the ``return``, move it to
+a :c:macro:`Py_mod_create` or :c:macro:`Py_mod_exec` slot function.
+See the :ref:`PyInit documentation <extension-pyinit>` for related information.
+
+The function references a ``PyModuleDef`` object (``<moddef>`` in the code
+above).
+Its definition should be similar to the following, with different values
+and perhaps some fields unnnamed or left out:
+
+.. code-block::
+   :class: bad
+
+   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,
+   };
+
+Remove this definition and the ``PyInit`` function (or put them in
+an ``#ifndef Py_TARGET_ABI3T`` block, to retain backwards compatibility),
+and replace them with the following:
+
+.. code-block::
+   :class: good
+
+   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;
+   }
+
+Leave out any fields that were missing (except the new :c:macro:`Py_mod_abi`),
+and substitute your own values.
+
+See the :c:type:`PySlot` and :c:ref:`export hook <extension-export-hook>`
+documentation for details on this API.
+
+Associated ``PyModuleDef``
+--------------------------
+
+Since the new API does not use a :c:type:`!PyModuleDef` structure, a definition
+will not be associated with the resulting module.
+This changes the behavior of the following functions:
+
+- :c:func:`PyModule_GetDef`
+- :c:func:`PyType_GetModuleByDef`
+
+Check your code for these.
+If you do not use them, you can skip this section.
+
+These functions are typically used for two purposes:
+
+1. To get the definition the module was created with.
+   This is no longer possible using the new API.
+   Modules no longer keep a reference to the definition, so you will need to
+   figure out a different way to pass the relevant data around.
+
+.. _abi3t-migration-module-token:
+
+2. To check if a given module object is “yours”.
+   This use case is now served by :ref:`module tokens <ext-module-token>` --
+   opaque pointers that identify a module.
+   To use a token, declare (or reuse) a unique static variable, for example:
+
+   .. code-block::
+      :class: good
+
+      static char my_token;
+
+   and add a pointer to it in a new entry to your module's ``PySlot`` array:
+
+   .. code-block::
+      :class: good
+      :emphasize-lines: 3
+
+      static PySlot my_slot_array[] = {
+         ...
+         PySlot_STATIC_DATA(Py_mod_token, &my_token),
+         PySlot_END
+      }
+
+   Then, switch from :c:func:`PyModule_GetDef` calls such as:
+
+   .. code-block::
+      :class: bad
+
+      PyModuleDef *def = PyModule_GetDef(module);
+
+   to :c:func:`PyModule_GetToken` (which uses an output argument and may fail
+   with an exception):
+
+   .. code-block::
+      :class: good
+
+      void *token;
+      if (PyModule_GetToken(module, &token) < 0) {
+          /* handle error */
+      }
+
+   and from :c:func:`PyType_GetModuleByDef` calls such as:
+
+   .. code-block::
+      :class: bad
+
+      PyObject *module = PyType_GetModuleByDef(type, my_def);
+      /* handle error; use module */
+
+   to :c:func:`PyType_GetModuleByToken` (which returns a strong reference):
+
+   .. code-block::
+      :class: good
+
+      PyObject *module = PyType_GetModuleByToken(type, my_token);
+      /* handle error; use module */
+      Py_XDECREF(module);
+
+``PyObject`` opaqueness
+=======================
+
+The :c:type:`PyObject` and :c:type:`PyVarObject` structures are opaque
+in ``abi3t``.
+
+Accessing their members is prohibited.
+If you do this, switch to getter/setter functions mentioned in
+their documentation:
+
+- :c:member:`PyObject.ob_type`
+- :c:member:`PyObject.ob_refcnt`
+- :c:member:`PyVarObject.ob_size`
+
+Also, the *size* of the :c:type:`PyObject` structures is
+unknown to the compiler.
+It can -- and *does* -- change between different CPython builds.
+
+.. note::
+
+   While the size is available at runtime (for example as
+   ``sys.getsizeof(object())`` in Python code), you should resist the
+   temptation to calculate pointer offsets from it.
+   The object memory layout is subject to change in future
+   ``abi3t`` implementations.
+
+
+Custom type definitions
+-----------------------
+
+Since :c:type:`!PyObject` is opaque, the traditional way of defining
+custom types no longer works:
+
+.. code-block::
+   :class: bad
+
+   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),
+      ...
+   };
+
+Most likely, all your class definitions, *and* all code that accesses
+your classes' data, will need to be rewritten.
+This will probably be the biggest change you need to support ``abi3t``.
+
+For each such type, instead of defining a ``struct`` for the entire instance,
+define one with only the “additional” fields -- ones specific to your class,
+not its superclasses:
+
+.. code-block::
+   :class: good
+
+   typedef struct {
+      int my_data;
+   } CustomObjectData;
+
+Change the name.
+Almost all code that uses the struct will need to change
+(notably, pointers to the new structure cannot be cast to/from ``PyObject*``),
+and changing the name will highlight the usages as compiler errors.
+(If you use ``typeof``, C++ ``auto``, or similar ways to avoid
+typing the type name, this won't work. Be extra careful, and consider running
+tools to detect undefined behavior.)
+
+Then, to create the class, use *negative* ``basicsize`` to indicate
+“extra” storage space rather than *total* instance size:
+
+.. code-block::
+   :class: good
+
+   static PyType_Spec CustomType_spec = {
+      ...
+      .basicsize = -sizeof(CustomObjectData), /* note the minus sign */
+      ...
+   };
+
+If you use :c:macro:`Py_tp_members`, set the :c:macro:`Py_RELATIVE_OFFSET`
+flag on each member and specify the :c:member:`~PyMemberDef.offset`
+relative to your new struct.
+
+
+Custom type data access
+-----------------------
+
+Then comes the hard part: in all code that needs to access this struct,
+you will need an additional :c:func:`PyObject_GetTypeData` call to
+retrieve a ``CustomObjectData *`` pointer from ``PyObject *``:
+
+.. code-block::
+   :class: good
+
+   PyObject *obj = ...;
+   CustomObjectData *data = PyObject_GetTypeData(obj, cls);
+
+Note that this call requires the *type object* for your class (``cls``).
+
+If your class is not subclassable (that is, it does not use the
+:c:macro:`Py_TPFLAGS_BASETYPE` flag), ``cls`` will be ``Py_TYPE(obj)``.
+Otherwise, **DO NOT USE** ``Py_TYPE`` with :c:func:`!PyObject_GetTypeData`:
+it might return memory reserved to an unrelated subclass!
+For example, if a user makes a subclass like this:
+
+.. code-block:: python
+
+   class Sub(YourCustomClass):
+      __slots__ = ('a', 'b')
+
+then ``Py_TYPE(obj)`` is ``YourCustomClass``, and the underlying memory may
+look like this:
+
+.. code-block:: text
+
+   ╭─ PyObject *obj
+   │              ╭─ the pointer you want
+   │              │                    ╭─ PyObject_GetTypeData(obj, Py_TYPE(obj))
+   ▼              ▼                    ▼
+   ┌──────────┬───┬────────────────┬───┬─────────────┬───┬─────────────┐
+   │ PyObject │...│ CustomTypeData │...│ PyObject *a │...│ PyObject *b │
+   └──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘
+
+(Ellipses indicate possible padding.
+Note that this memory layout is not guaranteed: future versions of Python may
+add different padding or even switch the order of the structures.)
+
+There are two main ways to get the right class:
+
+- In instance methods, your implementation may use the :c:type:`PyCMethod`
+  signature (and the :c:macro:`METH_METHOD` bit in
+  :c:member:`PyMethodDef.ml_flags`),
+  and get the class as the ``defining_class`` argument.
+- Otherwise, give your class a unique static token using the
+  :c:macro:`Py_tp_token` slot, and use:
+
+  .. code-block::
+     :class: good
+
+     PyTypeObject cls;
+     if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, &cls) < 0) {
+         /* handle error */
+     }
+     CustomObjectData *data = PyObject_GetTypeData(obj, cls);
+
+  Type tokens work similarly to module tokens covered :ref:`earlier in this
+  guide <abi3t-migration-module-token>`.
+
+
+
+Avoid build-time conditionals
+=============================
+
+Check your code for API that identifies the version of Python used to
+*build* your extension.
+This no longer corresponds to the Python your extension runs on, so code
+that uses this information often needs changing.
+The macros to check for are:
+
+- :c:macro:`PY_VERSION_HEX`, :c:macro:`PY_MAJOR_VERSION`,
+  :c:macro:`PY_MINOR_VERSION`:
+
+  - to get the run-time version, use :c:data:`Py_Version`;
+  - to determine what C API is available, use :c:macro:`Py_TARGET_ABI3T`.
+    This macro is set to the minimum supported version.
+
+- :c:macro:`Py_GIL_DISABLED`: under ``abi3t``, this macro is always defined.
+  Code that works with free-threaded Python *should* also work with
+  the GIL enabled (since the GIL can be enabled at run time),
+  and usually *does* (unless it, for some reason, requires more than one
+  :term:`attached thread state <attached thread state>` at one time).
+
+
+Further code changes
+====================
+
+If you are still left with compiler errors or warnings, find a way to fix them.
+Alas, this guide is limited, and cannot cover all possible code
+changes extensions may need.
+
+If you find a problem that other extension authors might run into,
+consider :ref:`reporting an issue <reporting-documentation-bugs>` (or sending
+a pull request) for this guide.
+
+It is possible your issue cannot be fixed for the current version of ``abi3t``.
+In that case, reporting it may help it get prioritized for the next version
+of CPython.
+
+
+.. _abi3t-migration-tagging:
+
+Tagging and distribution
+========================
+
+If you are using a build tool with ``abi3t`` support, your extension is ready,
+but you might want to check that it was built correctly.
+
+Extensions built with ``abi3t`` should have the following extension:
+
+- On Windows: ``.pyd`` (like any other extension);
+- Linux, macOS, and other systems that use the ``.so`` suffix: ``.abi3t.so``
+  (**not** ``.cpython-315t.so`` or ``.abi3.so``).
+  Note that both free-threaded and non-free-threaded builds will
+  load ``.abi3t.so`` extensions;
+- Other systems: consult your distributor, and perhaps update this guide.
+
+If you distribute the extension as a *wheel*, use the following tags:
+
+* Python tag: :samp:`cp3{XX}`, where *XX* is the minimum Python version
+  the extension is built for.
+  (For example, ``cp315`` if you set ``Py_TARGET_ABI3T`` to ``0x30f0000``.
+  See :ref:`abi3-compiling` for more values.)
+* ABI tag: ``abi3.abi3t``. This is a *compressed tag set* that indicates
+  support for both non-free-threaded and free-threaded builds.
+
+For example, the wheel filename may look like this:
+
+.. code-block:: text
+
+   myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl
+
+.. seealso:: `Platform Compatibility Tags <https://packaging.python.org/en/latest/specifications/platform-compatibility-tags/>`__ in the PyPA package distribution metadata.
+
+If the filename or tags are incorrect, fix them.
+
+
+Testing
+=======
+
+Note that when you build an extension compatible with multiple versions of
+CPython, you should always *test* it with each version it supports (for
+example, 3.15, 3.16, and so on).
+Stable ABI only guarantees *ABI* compatibility; there may also be behavior
+changes -- both intentional ones (covered by :pep:`387`) and bugs.
+
+Be sure to run tests on both free-threaded and non-free-threaded builds
+of CPython.
+
+If they pass, congratulations! You have an ``abi3t`` extension.
index 81fc7e63f35bd79168de1461301ba617808a3814..57e2d6e0752447f43b8c2a93c291e141586b65eb 100644 (file)
@@ -37,6 +37,7 @@ Python Library Reference.
    mro.rst
    free-threading-python.rst
    free-threading-extensions.rst
+   abi3t-migration.rst
    remote_debugging.rst
 
 General:
@@ -61,6 +62,7 @@ Advanced development:
 * :ref:`freethreading-python-howto`
 * :ref:`freethreading-extensions-howto`
 * :ref:`isolating-extensions-howto`
+* :ref:`abi3t-migration-howto`
 * :ref:`python_2.3_mro`
 * :ref:`socket-howto`
 * :ref:`timerfd-howto`
index 1409c77aed9c6bc17c2ab639bcc0788ad0bfcb67..0e762042979c2b7902a294ce330fb93795d5fc85 100644 (file)
@@ -394,7 +394,7 @@ class VersionHexCheatsheet(SphinxDirective):
         current_minor = int(self.config.version.removeprefix('3.'))
         for minor in range(current_minor - 5, current_minor + 1):
             value = (3 << 24) | (minor << 16)
-            content.append(f'    {value:#x}  /* Py_PACK_VERSION(3.{minor}) */')
+            content.append(f'    {value:#x}  /* Py_PACK_VERSION(3,{minor}) */')
         node = nodes.paragraph()
         self.state.nested_parse(StringList(content), 0, node)
         return [node]
index 0f76c66144d6899d75add80a2b36402d6133cb2d..ccfe0f59cb5615c436044443b921d0969eab89f3 100644 (file)
@@ -513,10 +513,6 @@ specifically:
   purpose in :pep:`793`, with a new :c:type:`PySlot` structure
   introduced in :pep:`820`.
 
-The reference documentation for these features is complete, but currently
-aimed at early adopters.
-A migration guide is planned for an upcoming beta release.
-
 Note that Stable ABI does not offer all the functionality that CPython
 has to offer.
 Extensions that cannot switch to ``abi3t`` should continue to build for
@@ -532,6 +528,9 @@ If not using a build tool -- or when writing such a tool -- you can select
 ``abi3t`` by setting the macro :c:macro:`!Py_TARGET_ABI3T` as discussed
 in :ref:`abi3-compiling`.
 
+A practical :ref:`migration guide <abi3t-migration-howto>` for switching to
+``abi3t`` is available.
+
 .. seealso:: :pep:`803` for further details.