From b900611472aa95440175c5de857702703ce62db8 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 16 Mar 2026 09:06:08 -0600 Subject: [PATCH 1/9] allow .abi3t.so extensions --- Python/dynload_shlib.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Python/dynload_shlib.c b/Python/dynload_shlib.c index 583c9b752dfd90..2e1455fbe232f4 100644 --- a/Python/dynload_shlib.c +++ b/Python/dynload_shlib.c @@ -44,7 +44,10 @@ const char *_PyImport_DynLoadFiletab[] = { #ifdef ALT_SOABI "." ALT_SOABI ".so", #endif +#ifndef Py_GIL_DISABLED ".abi" PYTHON_ABI_STRING ".so", +#endif /* Py_GIL_DISABLED */ + ".abi" PYTHON_ABI_STRING "t.so", ".so", #endif /* __CYGWIN__ */ NULL, From 84043591a8e3e9339de5c2dcc29710aa90f3d009 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 16 Mar 2026 10:39:09 -0600 Subject: [PATCH 2/9] add test --- Lib/test/test_importlib/extension/test_finder.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py index cdc8884d668a66..15ca4a83b0c611 100644 --- a/Lib/test/test_importlib/extension/test_finder.py +++ b/Lib/test/test_importlib/extension/test_finder.py @@ -60,6 +60,19 @@ def test_failure(self): self.assertIsNone(self.find_spec('asdfjkl;')) +class ExtensionSuffixTest(unittest.TestCase): + def test_abi3_extension_suffixes(self): + suffixes = machinery.EXTENSION_SUFFIXES + if 'win32' in sys.platform: + self.assertIn(".pyd", suffixes) + if 'cygwin' in sys.platform: + pass + else: + if not support.Py_GIL_DISABLED: + self.assertIn(".abi3.so", suffixes) + self.assertIn(".abi3t.so", suffixes) + + (Frozen_FinderTests, Source_FinderTests ) = util.test_both(FinderTests, machinery=machinery) From 1d0b61be2d794998cf6f314c7bcfc08a5f3f0501 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Mon, 16 Mar 2026 10:43:28 -0600 Subject: [PATCH 3/9] fix test --- Lib/test/test_importlib/extension/test_finder.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py index 15ca4a83b0c611..2f774c95d8f8b7 100644 --- a/Lib/test/test_importlib/extension/test_finder.py +++ b/Lib/test/test_importlib/extension/test_finder.py @@ -1,4 +1,4 @@ -from test.support import is_apple_mobile +from test.support import is_apple_mobile, Py_GIL_DISABLED from test.test_importlib import abc, util machinery = util.import_importlib('importlib.machinery') @@ -59,16 +59,14 @@ def test_module(self): def test_failure(self): self.assertIsNone(self.find_spec('asdfjkl;')) - -class ExtensionSuffixTest(unittest.TestCase): def test_abi3_extension_suffixes(self): - suffixes = machinery.EXTENSION_SUFFIXES + suffixes = self.machinery.EXTENSION_SUFFIXES if 'win32' in sys.platform: self.assertIn(".pyd", suffixes) - if 'cygwin' in sys.platform: + elif 'cygwin' in sys.platform: pass else: - if not support.Py_GIL_DISABLED: + if not Py_GIL_DISABLED: self.assertIn(".abi3.so", suffixes) self.assertIn(".abi3t.so", suffixes) From dc8d4b814453cfa8ae609dfede70bc7d8564d583 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 30 Mar 2026 15:05:16 +0200 Subject: [PATCH 4/9] Test that free-threaded builds don't have .abi3.so in suffixes --- Lib/test/test_importlib/extension/test_finder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py index 2f774c95d8f8b7..cf2256805b8e11 100644 --- a/Lib/test/test_importlib/extension/test_finder.py +++ b/Lib/test/test_importlib/extension/test_finder.py @@ -66,7 +66,9 @@ def test_abi3_extension_suffixes(self): elif 'cygwin' in sys.platform: pass else: - if not Py_GIL_DISABLED: + if Py_GIL_DISABLED: + self.assertNotIn(".abi3.so", suffixes) + else: self.assertIn(".abi3.so", suffixes) self.assertIn(".abi3t.so", suffixes) From 4c087a46202537dc5e277a7fc6f30dd2ca4607d8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 30 Mar 2026 15:47:42 +0200 Subject: [PATCH 5/9] test_capi: Replace _Py_OPAQUE_PYOBJECT by Py_TARGET_ABI3T --- Lib/test/test_cext/__init__.py | 17 ++++++++--------- Lib/test/test_cext/setup.py | 12 +++++------- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/Lib/test/test_cext/__init__.py b/Lib/test/test_cext/__init__.py index a52c2241f5d9d4..1958c44e2b64ef 100644 --- a/Lib/test/test_cext/__init__.py +++ b/Lib/test/test_cext/__init__.py @@ -38,15 +38,15 @@ def test_build(self): self.check_build('_test_cext') def check_build(self, extension_name, std=None, limited=False, - opaque_pyobject=False): + abi3t=False): venv_dir = 'env' with support.setup_venv_with_pip_setuptools(venv_dir) as python_exe: self._check_build(extension_name, python_exe, std=std, limited=limited, - opaque_pyobject=opaque_pyobject) + abi3t=abi3t) def _check_build(self, extension_name, python_exe, std, limited, - opaque_pyobject): + abi3t): pkg_dir = 'pkg' os.mkdir(pkg_dir) shutil.copy(SETUP, os.path.join(pkg_dir, os.path.basename(SETUP))) @@ -60,8 +60,8 @@ def run_cmd(operation, cmd): env['CPYTHON_TEST_STD'] = std if limited: env['CPYTHON_TEST_LIMITED'] = '1' - if opaque_pyobject: - env['CPYTHON_TEST_OPAQUE_PYOBJECT'] = '1' + if abi3t: + env['CPYTHON_TEST_ABI3T'] = '1' env['CPYTHON_TEST_EXT_NAME'] = extension_name env['TEST_INTERNAL_C_API'] = str(int(self.TEST_INTERNAL_C_API)) if support.verbose: @@ -116,10 +116,9 @@ def test_build_limited_c11(self): def test_build_c11(self): self.check_build('_test_c11_cext', std='c11') - def test_build_opaque_pyobject(self): - # Test with _Py_OPAQUE_PYOBJECT - self.check_build('_test_limited_opaque_cext', limited=True, - opaque_pyobject=True) + def test_build_abi3t(self): + # Test with Py_TARGET_ABI3T + self.check_build('_test_abi3t', abi3t=True) @unittest.skipIf(support.MS_WINDOWS, "MSVC doesn't support /std:c99") def test_build_c99(self): diff --git a/Lib/test/test_cext/setup.py b/Lib/test/test_cext/setup.py index db43f6fb17a132..7262a110d83415 100644 --- a/Lib/test/test_cext/setup.py +++ b/Lib/test/test_cext/setup.py @@ -59,7 +59,7 @@ def main(): std = os.environ.get("CPYTHON_TEST_STD", "") module_name = os.environ["CPYTHON_TEST_EXT_NAME"] limited = bool(os.environ.get("CPYTHON_TEST_LIMITED", "")) - opaque_pyobject = bool(os.environ.get("CPYTHON_TEST_OPAQUE_PYOBJECT", "")) + abi3t = bool(os.environ.get("CPYTHON_TEST_ABI3T", "")) internal = bool(int(os.environ.get("TEST_INTERNAL_C_API", "0"))) sources = [SOURCE] @@ -91,14 +91,12 @@ def main(): # CC env var overrides sysconfig CC variable in setuptools os.environ['CC'] = cmd - # Define Py_LIMITED_API macro + # Define opt-in macros if limited: - version = sys.hexversion - cflags.append(f'-DPy_LIMITED_API={version:#x}') + cflags.append(f'-DPy_LIMITED_API={sys.hexversion:#x}') - # Define _Py_OPAQUE_PYOBJECT macro - if opaque_pyobject: - cflags.append(f'-D_Py_OPAQUE_PYOBJECT') + if abi3t: + cflags.append(f'-DPy_TARGET_ABI3T={sys.hexversion:#x}') if internal: cflags.append('-DTEST_INTERNAL_C_API=1') From a00734884dca3ae7f52d5853118fb7f6b9d0a0b4 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Mon, 30 Mar 2026 15:32:50 +0200 Subject: [PATCH 6/9] Select the stable ABI free-threaded builds --- Include/Python.h | 4 ---- Include/patchlevel.h | 28 ++++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/Include/Python.h b/Include/Python.h index 17cbc083241514..e6e5cab67e2045 100644 --- a/Include/Python.h +++ b/Include/Python.h @@ -47,10 +47,6 @@ #endif #if defined(Py_GIL_DISABLED) -# if defined(Py_LIMITED_API) && !defined(_Py_OPAQUE_PYOBJECT) -# error "Py_LIMITED_API is not currently supported in the free-threaded build" -# endif - # if defined(_MSC_VER) # include // __readgsqword() # endif diff --git a/Include/patchlevel.h b/Include/patchlevel.h index 7cffd74125f1b4..6dafc3cf96404c 100644 --- a/Include/patchlevel.h +++ b/Include/patchlevel.h @@ -61,4 +61,32 @@ #define PYTHON_ABI_VERSION 3 #define PYTHON_ABI_STRING "3" + +/* Stable ABI for free-threaded builds (introduced in PEP 803) + is enabled by one of: + - Py_TARGET_ABI3T, or + - Py_LIMITED_API and Py_GIL_DISABLED. + "Output" macros to be used internally: + - Py_LIMITED_API (defines the subset of API we expose) + - _Py_OPAQUE_PYOBJECT (additionally hides what's ABI-incompatible between + free-threaded & GIL) + (Don't use Py_TARGET_ABI3T directly; we merge into the above two here. + We do set it for users' convenience though.) + */ +#if defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) \ + && !defined(Py_TARGET_ABI3T) +# define Py_TARGET_ABI3T Py_LIMITED_API +#endif +#if defined(Py_TARGET_ABI3T) +# define _Py_OPAQUE_PYOBJECT +# if !defined(Py_LIMITED_API) +# define Py_LIMITED_API Py_TARGET_ABI3T +# elif Py_LIMITED_API > Py_TARGET_ABI3T + // if both are defined, use the *lower* version, + // i.e. maximum compatibility +# undef Py_LIMITED_API +# define Py_LIMITED_API Py_TARGET_ABI3T +# endif +#endif + #endif //_Py_PATCHLEVEL_H From 6d62f4a8001f964c3a507591b5afca3ff9711725 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 31 Mar 2026 11:28:52 +0200 Subject: [PATCH 7/9] Add minimal docs --- Doc/c-api/stable.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Doc/c-api/stable.rst b/Doc/c-api/stable.rst index f5e6b7ad157e99..f8b41f6d87f975 100644 --- a/Doc/c-api/stable.rst +++ b/Doc/c-api/stable.rst @@ -88,6 +88,16 @@ Contents of the Limited API are :ref:`listed below `. You can also define ``Py_LIMITED_API`` to ``3``. This works the same as ``0x03020000`` (Python 3.2, the version that introduced Limited API). +.. c:macro:: Py_TARGET_ABI3T + + Define this macro before including ``Python.h`` to opt in to only use + the Limited API for :term:`free-threaded builds `, + and to select the Limited API version. + + .. seealso:: :pep:`803` + + .. versionadded:: next + .. _stable-abi: From db32325100bcab9822251a0310f0d27d83f2f877 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 31 Mar 2026 11:33:05 +0200 Subject: [PATCH 8/9] Allow "_d.pyd" on Windows --- Lib/test/test_importlib/extension/test_finder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_importlib/extension/test_finder.py b/Lib/test/test_importlib/extension/test_finder.py index cf2256805b8e11..dc77fa78a203fd 100644 --- a/Lib/test/test_importlib/extension/test_finder.py +++ b/Lib/test/test_importlib/extension/test_finder.py @@ -62,7 +62,8 @@ def test_failure(self): def test_abi3_extension_suffixes(self): suffixes = self.machinery.EXTENSION_SUFFIXES if 'win32' in sys.platform: - self.assertIn(".pyd", suffixes) + # Either "_d.pyd" or ".pyd" must be in suffixes + self.assertTrue({"_d.pyd", ".pyd"}.intersection(suffixes)) elif 'cygwin' in sys.platform: pass else: From 323c9fc0bf88d4de9a3e2575691336cb39318de8 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 1 Apr 2026 13:06:21 +0200 Subject: [PATCH 9/9] Reword note on Py_TARGET_ABI3T --- Include/patchlevel.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Include/patchlevel.h b/Include/patchlevel.h index 6dafc3cf96404c..154bdb0721d3d1 100644 --- a/Include/patchlevel.h +++ b/Include/patchlevel.h @@ -70,8 +70,8 @@ - Py_LIMITED_API (defines the subset of API we expose) - _Py_OPAQUE_PYOBJECT (additionally hides what's ABI-incompatible between free-threaded & GIL) - (Don't use Py_TARGET_ABI3T directly; we merge into the above two here. - We do set it for users' convenience though.) + (Don't use Py_TARGET_ABI3T directly: it's currently only used to set these + 2 macros. It's also available for users' convenience.) */ #if defined(Py_LIMITED_API) && defined(Py_GIL_DISABLED) \ && !defined(Py_TARGET_ABI3T)