From 072673a1180f5edf2c3dbe50f960fe03d4cfd7e8 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Mar 2026 01:47:28 +0100 Subject: [PATCH 1/5] gh-81773: Avoid parsing sys.version string in sysconfig No longer parse sys.version string to create sysconfig._PY_VERSION. Add _sysconfig._PY_VERSION constant. --- Lib/sysconfig/__init__.py | 5 ++--- Lib/test/test_sysconfig.py | 8 ++++++++ Modules/_sysconfig.c | 9 +++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py index 6507a7b5b0f695..7bb6051749577b 100644 --- a/Lib/sysconfig/__init__.py +++ b/Lib/sysconfig/__init__.py @@ -1,5 +1,6 @@ """Access to Python's configuration information.""" +import _sysconfig import os import sys import threading @@ -172,7 +173,7 @@ def joinuser(*args): _SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include', 'scripts', 'data') -_PY_VERSION = sys.version.split()[0] +_PY_VERSION = _sysconfig._PY_VERSION _PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}' _PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}' _BASE_PREFIX = os.path.normpath(sys.base_prefix) @@ -385,7 +386,6 @@ def _init_non_posix(vars): """Initialize the module as appropriate for NT""" # set basic install directories import _winapi - import _sysconfig vars['LIBDEST'] = get_path('stdlib') vars['BINLIBDEST'] = get_path('platstdlib') vars['INCLUDEPY'] = get_path('include') @@ -665,7 +665,6 @@ def get_platform(): For other non-POSIX platforms, currently just returns :data:`sys.platform`.""" if os.name == 'nt': - import _sysconfig platform = _sysconfig.get_platform() if platform: return platform diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index e43f91eb9238f9..09c1fd27782a20 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -741,6 +741,14 @@ def test_sysconfig_config_vars_no_prefix_cache(self): self.assertEqual(config_vars['exec_prefix'], sys.exec_prefix) self.assertEqual(config_vars['platbase'], sys.exec_prefix) + def test_py_version(self): + config_vars = sysconfig.get_config_vars() + py_version = config_vars['py_version'] + self.assertIsInstance(py_version, str) + ver = sys.version_info + version = f'{ver.major}.{ver.minor}.{ver.micro}' + self.assertStartsWith(py_version, version) + class MakefileTests(unittest.TestCase): diff --git a/Modules/_sysconfig.c b/Modules/_sysconfig.c index bcb9d108174f43..e9afbdb92f9cb0 100644 --- a/Modules/_sysconfig.c +++ b/Modules/_sysconfig.c @@ -117,6 +117,14 @@ _sysconfig_get_platform_impl(PyObject *module) #endif // MS_WINDOWS +static int +sysconfig_module_exec(PyObject *module) +{ + return PyModule_Add(module, "_PY_VERSION", + PyUnicode_FromString(PY_VERSION)); +} + + PyDoc_STRVAR(sysconfig__doc__, "A helper for the sysconfig module."); @@ -127,6 +135,7 @@ static struct PyMethodDef sysconfig_methods[] = { }; static PyModuleDef_Slot sysconfig_slots[] = { + {Py_mod_exec, sysconfig_module_exec}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0, NULL} From a4c266469508c65330790fc633ba406282da691c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Thu, 19 Mar 2026 01:59:45 +0100 Subject: [PATCH 2/5] Rename to _sysconfig.PY_VERSION --- Lib/sysconfig/__init__.py | 2 +- Modules/_sysconfig.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py index 7bb6051749577b..10ba9bea8a3c6b 100644 --- a/Lib/sysconfig/__init__.py +++ b/Lib/sysconfig/__init__.py @@ -173,7 +173,7 @@ def joinuser(*args): _SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include', 'scripts', 'data') -_PY_VERSION = _sysconfig._PY_VERSION +_PY_VERSION = _sysconfig.PY_VERSION _PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}' _PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}' _BASE_PREFIX = os.path.normpath(sys.base_prefix) diff --git a/Modules/_sysconfig.c b/Modules/_sysconfig.c index e9afbdb92f9cb0..79945675a2baba 100644 --- a/Modules/_sysconfig.c +++ b/Modules/_sysconfig.c @@ -120,7 +120,7 @@ _sysconfig_get_platform_impl(PyObject *module) static int sysconfig_module_exec(PyObject *module) { - return PyModule_Add(module, "_PY_VERSION", + return PyModule_Add(module, "PY_VERSION", PyUnicode_FromString(PY_VERSION)); } From 01116388106af39dbdbeae8b59ca20f227fad00d Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 23 Mar 2026 23:09:56 +0100 Subject: [PATCH 3/5] Reuse _sysconfig.config_vars() to get the Python version * Move also py_version_short and py_version_nodot to _sysconfig. * Use _sysconfig.config_vars() on all platforms, no only Windows. --- Lib/sysconfig/__init__.py | 22 ++++++++---------- Lib/test/test_sysconfig.py | 14 +++++++++--- Modules/_sysconfig.c | 46 ++++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 35 deletions(-) diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py index 10ba9bea8a3c6b..2de7ff332e3208 100644 --- a/Lib/sysconfig/__init__.py +++ b/Lib/sysconfig/__init__.py @@ -173,9 +173,6 @@ def joinuser(*args): _SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include', 'scripts', 'data') -_PY_VERSION = _sysconfig.PY_VERSION -_PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}' -_PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}' _BASE_PREFIX = os.path.normpath(sys.base_prefix) _BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix) # Mutex guarding initialization of _CONFIG_VARS. @@ -324,7 +321,8 @@ def get_makefile_filename(): return os.path.join(_PROJECT_BASE, "Makefile") if hasattr(sys, 'abiflags'): - config_dir_name = f'config-{_PY_VERSION_SHORT}{sys.abiflags}' + py_version_short = f'{sys.version_info[0]}.{sys.version_info[1]}' + config_dir_name = f'config-{py_version_short}{sys.abiflags}' else: config_dir_name = 'config' @@ -390,9 +388,6 @@ def _init_non_posix(vars): vars['BINLIBDEST'] = get_path('platstdlib') vars['INCLUDEPY'] = get_path('include') - # Add EXT_SUFFIX, SOABI, Py_DEBUG, and Py_GIL_DISABLED - vars.update(_sysconfig.config_vars()) - # NOTE: ABIFLAGS is only an emulated value. It is not present during build # on Windows. sys.abiflags is absent on Windows and vars['abiflags'] # is already widely used to calculate paths, so it should remain an @@ -410,7 +405,7 @@ def _init_non_posix(vars): vars['LIBRARY'] = os.path.basename(_safe_realpath(dllhandle)) vars['LDLIBRARY'] = vars['LIBRARY'] vars['EXE'] = '.exe' - vars['VERSION'] = _PY_VERSION_SHORT_NO_DOT + vars['VERSION'] = vars['py_version_nodot'] vars['BINDIR'] = os.path.dirname(_safe_realpath(sys.executable)) # No standard path exists on Windows for this, but we'll check # whether someone is imitating a POSIX-like layout @@ -505,6 +500,10 @@ def _init_config_vars(): global _CONFIG_VARS _CONFIG_VARS = {} + # Add py_version, Py_DEBUG, and Py_GIL_DISABLED. + # On Windows, add also EXT_SUFFIX and SOABI. + _CONFIG_VARS.update(_sysconfig.config_vars()) + prefix = os.path.normpath(sys.prefix) exec_prefix = os.path.normpath(sys.exec_prefix) base_prefix = _BASE_PREFIX @@ -530,9 +529,6 @@ def _init_config_vars(): # Distutils. _CONFIG_VARS['prefix'] = prefix _CONFIG_VARS['exec_prefix'] = exec_prefix - _CONFIG_VARS['py_version'] = _PY_VERSION - _CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT - _CONFIG_VARS['py_version_nodot'] = _PY_VERSION_SHORT_NO_DOT _CONFIG_VARS['installed_base'] = base_prefix _CONFIG_VARS['base'] = prefix _CONFIG_VARS['installed_platbase'] = base_exec_prefix @@ -739,11 +735,11 @@ def get_platform(): def get_python_version(): - return _PY_VERSION_SHORT + return get_config_var('py_version_short') def _get_python_version_abi(): - return _PY_VERSION_SHORT + get_config_var("abi_thread") + return get_config_var('py_version_short') + get_config_var("abi_thread") def expand_makefile_vars(s, vars): diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index 09c1fd27782a20..434beeffc8299e 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -743,11 +743,19 @@ def test_sysconfig_config_vars_no_prefix_cache(self): def test_py_version(self): config_vars = sysconfig.get_config_vars() - py_version = config_vars['py_version'] - self.assertIsInstance(py_version, str) + self.assertIsInstance(config_vars['py_version'], str) ver = sys.version_info version = f'{ver.major}.{ver.minor}.{ver.micro}' - self.assertStartsWith(py_version, version) + # py_version can be longer such as "3.15.0a7+" instead of "3.15.0" + self.assertStartsWith(config_vars['py_version'], version) + + self.assertIsInstance(config_vars['py_version_short'], str) + self.assertEqual(config_vars['py_version_short'], + f'{ver.major}.{ver.minor}') + + self.assertIsInstance(config_vars['py_version_nodot'], str) + self.assertEqual(config_vars['py_version_nodot'], + f'{ver.major}{ver.minor}') class MakefileTests(unittest.TestCase): diff --git a/Modules/_sysconfig.c b/Modules/_sysconfig.c index 79945675a2baba..a5574fd506c340 100644 --- a/Modules/_sysconfig.c +++ b/Modules/_sysconfig.c @@ -17,7 +17,13 @@ module _sysconfig #include "clinic/_sysconfig.c.h" -#ifdef MS_WINDOWS + +#define py_version_short() \ + (Py_STRINGIFY(PY_MAJOR_VERSION) "." Py_STRINGIFY(PY_MINOR_VERSION)) +#define py_version_nodot() \ + (Py_STRINGIFY(PY_MAJOR_VERSION) Py_STRINGIFY(PY_MINOR_VERSION)) + + static int add_string_value(PyObject *dict, const char *key, const char *str_value) { @@ -29,7 +35,7 @@ add_string_value(PyObject *dict, const char *key, const char *str_value) Py_DECREF(value); return err; } -#endif + /*[clinic input] @permit_long_summary @@ -47,14 +53,23 @@ _sysconfig_config_vars_impl(PyObject *module) return NULL; } + // Python version + if (add_string_value(config, "py_version", PY_VERSION) < 0) { + goto error; + } + if (add_string_value(config, "py_version_short", py_version_short()) < 0) { + goto error; + } + if (add_string_value(config, "py_version_nodot", py_version_nodot()) < 0) { + goto error; + } + #ifdef MS_WINDOWS if (add_string_value(config, "EXT_SUFFIX", PYD_TAGGED_SUFFIX) < 0) { - Py_DECREF(config); - return NULL; + goto error; } if (add_string_value(config, "SOABI", PYD_SOABI) < 0) { - Py_DECREF(config); - return NULL; + goto error; } #endif @@ -64,8 +79,7 @@ _sysconfig_config_vars_impl(PyObject *module) PyObject *py_gil_disabled = _PyLong_GetZero(); #endif if (PyDict_SetItemString(config, "Py_GIL_DISABLED", py_gil_disabled) < 0) { - Py_DECREF(config); - return NULL; + goto error; } #ifdef Py_DEBUG @@ -74,11 +88,14 @@ _sysconfig_config_vars_impl(PyObject *module) PyObject *py_debug = _PyLong_GetZero(); #endif if (PyDict_SetItemString(config, "Py_DEBUG", py_debug) < 0) { - Py_DECREF(config); - return NULL; + goto error; } return config; + +error: + Py_DECREF(config); + return NULL; } #ifdef MS_WINDOWS @@ -117,14 +134,6 @@ _sysconfig_get_platform_impl(PyObject *module) #endif // MS_WINDOWS -static int -sysconfig_module_exec(PyObject *module) -{ - return PyModule_Add(module, "PY_VERSION", - PyUnicode_FromString(PY_VERSION)); -} - - PyDoc_STRVAR(sysconfig__doc__, "A helper for the sysconfig module."); @@ -135,7 +144,6 @@ static struct PyMethodDef sysconfig_methods[] = { }; static PyModuleDef_Slot sysconfig_slots[] = { - {Py_mod_exec, sysconfig_module_exec}, {Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED}, {Py_mod_gil, Py_MOD_GIL_NOT_USED}, {0, NULL} From e84a71222fc90067f022cb93c8f3690e4177788c Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 23 Mar 2026 23:09:56 +0100 Subject: [PATCH 4/5] Test sysconfig.get_python_version() --- Lib/test/test_sysconfig.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py index 434beeffc8299e..16a43848602f85 100644 --- a/Lib/test/test_sysconfig.py +++ b/Lib/test/test_sysconfig.py @@ -742,6 +742,7 @@ def test_sysconfig_config_vars_no_prefix_cache(self): self.assertEqual(config_vars['platbase'], sys.exec_prefix) def test_py_version(self): + # Test py_version config_vars = sysconfig.get_config_vars() self.assertIsInstance(config_vars['py_version'], str) ver = sys.version_info @@ -749,10 +750,14 @@ def test_py_version(self): # py_version can be longer such as "3.15.0a7+" instead of "3.15.0" self.assertStartsWith(config_vars['py_version'], version) + # Test py_version_short and get_python_version() self.assertIsInstance(config_vars['py_version_short'], str) self.assertEqual(config_vars['py_version_short'], f'{ver.major}.{ver.minor}') + self.assertEqual(sysconfig.get_python_version(), + config_vars['py_version_short']) + # Test py_version_nodot self.assertIsInstance(config_vars['py_version_nodot'], str) self.assertEqual(config_vars['py_version_nodot'], f'{ver.major}{ver.minor}') From 5c5ac7e3685ea97810603823e59e1b79b387ad25 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 23 Mar 2026 23:27:05 +0100 Subject: [PATCH 5/5] Coding style --- Lib/sysconfig/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/sysconfig/__init__.py b/Lib/sysconfig/__init__.py index 2de7ff332e3208..c2adbf7cd4ae4a 100644 --- a/Lib/sysconfig/__init__.py +++ b/Lib/sysconfig/__init__.py @@ -739,7 +739,7 @@ def get_python_version(): def _get_python_version_abi(): - return get_config_var('py_version_short') + get_config_var("abi_thread") + return get_config_var('py_version_short') + get_config_var('abi_thread') def expand_makefile_vars(s, vars):