From 14a1e7a1a0c45b72fa7a7f44d758cdcafb2adcf6 Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Tue, 12 May 2026 12:43:38 +0330 Subject: [PATCH 1/7] Compare pyenv.cfg version field with sys.version_info and emit a RuntimeWarning on minor-version mismatch. --- Lib/site.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/Lib/site.py b/Lib/site.py index cb1108dbaf1f818..e6f6a19290c09ca 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -796,6 +796,7 @@ def venv(known_paths): if candidate_conf: virtual_conf = candidate_conf system_site = "true" + version = None # Issue 25185: Use UTF-8, as that's what the venv module uses when # writing the file. with open(virtual_conf, encoding='utf-8') as f: @@ -808,6 +809,28 @@ def venv(known_paths): system_site = value.lower() elif key == 'home': sys._home = value + elif key == 'version': + version = value + + if version: + try: + major, minor = map(int, version.split(".")[:2]) + except ValueError: + major, minor = None + + if ( + major == sys.version_info.major + and minor is not None + and minor != sys.version_info.minor + and not hasattr(sys, "_venv_version_warning_emitted") + ): + _warn( + f"This virtual environment was created for Python {major}.{minor}, " + f"but the current interpreter is Python " + f"{sys.version_info.major}.{sys.version_info.minor}. " + "Consider running `python -m venv --upgrade` to update the environment.", + RuntimeWarning, + ) if sys.prefix != site_prefix: _warn(f'Unexpected value in sys.prefix, expected {site_prefix}, got {sys.prefix}', RuntimeWarning) From 1bca0c8aa0962f02a10524c7bd24ff8ffc79d45a Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Tue, 12 May 2026 12:49:47 +0330 Subject: [PATCH 2/7] Add news entry --- .../2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst new file mode 100644 index 000000000000000..474ae434f6885ea --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst @@ -0,0 +1,3 @@ +Warn when running a virtual environment created for a different minor Python +version than the current interpreter, and suggest using ``python -m venv +--upgrade``. From 5d0238f7454d6ee33a591afadc331b48c54f96cd Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Tue, 12 May 2026 15:11:42 +0330 Subject: [PATCH 3/7] Fix linting issues --- Lib/site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/site.py b/Lib/site.py index e6f6a19290c09ca..b83b3d1252d4a65 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -811,7 +811,7 @@ def venv(known_paths): sys._home = value elif key == 'version': version = value - + if version: try: major, minor = map(int, version.split(".")[:2]) From 64a8f28ae29bc77b90fa5bc33f832e4c5ecfba71 Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Tue, 12 May 2026 15:13:32 +0330 Subject: [PATCH 4/7] Fix linting --- Lib/site.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/site.py b/Lib/site.py index b83b3d1252d4a65..9ccc218d1910707 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -817,7 +817,6 @@ def venv(known_paths): major, minor = map(int, version.split(".")[:2]) except ValueError: major, minor = None - if ( major == sys.version_info.major and minor is not None From 0601cc5ae669cb9cc4c132486c09d9f9f4eb03d8 Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Thu, 21 May 2026 15:35:45 +0330 Subject: [PATCH 5/7] Update site.py --- Lib/site.py | 48 ++++++++++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/Lib/site.py b/Lib/site.py index 9ccc218d1910707..11d600707829983 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -796,7 +796,7 @@ def venv(known_paths): if candidate_conf: virtual_conf = candidate_conf system_site = "true" - version = None + version, version_info = None, None # Issue 25185: Use UTF-8, as that's what the venv module uses when # writing the file. with open(virtual_conf, encoding='utf-8') as f: @@ -811,25 +811,33 @@ def venv(known_paths): sys._home = value elif key == 'version': version = value - - if version: - try: - major, minor = map(int, version.split(".")[:2]) - except ValueError: - major, minor = None - if ( - major == sys.version_info.major - and minor is not None - and minor != sys.version_info.minor - and not hasattr(sys, "_venv_version_warning_emitted") - ): - _warn( - f"This virtual environment was created for Python {major}.{minor}, " - f"but the current interpreter is Python " - f"{sys.version_info.major}.{sys.version_info.minor}. " - "Consider running `python -m venv --upgrade` to update the environment.", - RuntimeWarning, - ) + elif key == 'version_info': + version_info = value + + for field_name, field_value in [ + ('version',version), ('version_info',version_info) + ]: + if field_value is not None: + try: + major, minor = map(int, field_value.split(".")[:2]) + except (ValueError, AttributeError): + _warn( + f"Malformed {field_name} string in pyvenv.cfg: {field_value!r}", + RuntimeWarning, + ) + else: + if ( + major == sys.version_info.major + and minor != sys.version_info.minor + ): + _warn( + f"This virtual environment was created for Python {major}.{minor}, " + f"but the current interpreter is Python " + f"{sys.version_info.major}.{sys.version_info.minor}. " + "Consider running `python -m venv --upgrade` to update the environment.", + RuntimeWarning, + ) + break if sys.prefix != site_prefix: _warn(f'Unexpected value in sys.prefix, expected {site_prefix}, got {sys.prefix}', RuntimeWarning) From e04f466a6012c9aba45d9f9f8d71fe9c806964e2 Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Thu, 21 May 2026 15:35:49 +0330 Subject: [PATCH 6/7] Add tests --- Lib/test/test_venv.py | 220 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 220 insertions(+) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 78461abcd69f337..d0442dd7244ec46 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -310,6 +310,226 @@ def test_sysconfig(self): out, err = check_output(cmd, encoding='utf-8') self.assertEqual(out.strip(), expected, err) + @requireVenvCreate + def test_version_mismatch_warning(self): + """ + Test that a warning is emitted when running a venv created for a + different minor Python version. + """ + rmtree(self.env_dir) + + wrong_minor = sys.version_info.minor + 1 + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + + new_version = f"{sys.version_info.major}.{wrong_minor}" + if 'version =' in cfg_content: + cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content) + + cfg_content += f'\nversion_info = {new_version}\n' + + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + + envpy = self.envpy(real_env_dir=True) + + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + + self.assertIn(f"Python {sys.version_info.major}.{wrong_minor}", proc.stderr) + self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr) + + @requireVenvCreate + def test_version_info_mismatch_warning(self): + """ + Test that a warning is emitted when version_info (used by virtualenv) + indicates a different minor version. + """ + rmtree(self.env_dir) + wrong_minor = sys.version_info.minor + 1 + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + + # Add only version_info, don't modify version + new_version = f"{sys.version_info.major}.{wrong_minor}" + cfg_content += f'\nversion_info = {new_version}\n' + + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + + envpy = self.envpy(real_env_dir=True) + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + + self.assertIn(f"Python {sys.version_info.major}.{wrong_minor}", proc.stderr) + self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr) + + @requireVenvCreate + def test_version_match_no_warning(self): + """ + Test that no warning is emitted when the venv version matches. + """ + rmtree(self.env_dir) + + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + expected_version = f"{sys.version_info.major}.{sys.version_info.minor}" + + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + envpy = self.envpy(real_env_dir=True) + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + + self.assertNotIn("Consider running `python -m venv --upgrade`", proc.stderr) + + @requireVenvCreate + def test_malformed_version_warning(self): + """ + Test that a warning is emitted on malformed version string + in pyenv.cfg + """ + rmtree(self.env_dir) + + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + + malformed_version = "not.a.version" + if 'version =' in cfg_content: + cfg_content = re.sub(r'version = .+', f'version = {malformed_version}', cfg_content) + + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + + envpy = self.envpy(real_env_dir=True) + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + self.assertIn("Malformed version string", proc.stderr) + self.assertIn(malformed_version, proc.stderr) + + @requireVenvCreate + def test_malformed_version_info_warning(self): + """ + Test that a warning is emitted on malformed version_info string + in pyenv.cfg + """ + rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + + malformed_version = "invalid.version" + cfg_content += f'\nversion_info = {malformed_version}\n' + + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + + envpy = self.envpy(real_env_dir=True) + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + + self.assertIn("Malformed version_info string", proc.stderr) + self.assertIn(malformed_version, proc.stderr) + + @requireVenvCreate + def test_conflicting_version_fields(self): + """ + Test behavior when both version and version_info are present + but contain different values. Should warn based on first mismatch found. + """ + rmtree(self.env_dir) + wrong_minor = sys.version_info.minor + 1 + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + + version_wrong = f"{sys.version_info.major}.{wrong_minor}" + if 'version =' in cfg_content: + cfg_content = re.sub(r'version = \d+\.\d+', f'version = {version_wrong}', cfg_content) + + version_info_wrong = f"{sys.version_info.major}.{wrong_minor + 1}" + cfg_content += f'\nversion_info = {version_info_wrong}\n' + + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + + envpy = self.envpy(real_env_dir=True) + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + + self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr) + self.assertEqual(proc.stderr.count("Consider running `python -m venv --upgrade`"), 1) + + @requireVenvCreate + def test_different_major_version_no_warning(self): + """ + Test that no warning is emitted when major version differs. + The warning should only trigger for same major, different minor. + """ + rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + cfg_path = self.get_env_file('pyvenv.cfg') + with open(cfg_path, 'r', encoding='utf-8') as f: + cfg_content = f.read() + + different_major = sys.version_info.major + 1 + new_version = f"{different_major}.{sys.version_info.minor}" + + if 'version =' in cfg_content: + cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content) + with open(cfg_path, 'w', encoding='utf-8') as f: + f.write(cfg_content) + + envpy = self.envpy(real_env_dir=True) + proc = subprocess.run( + [envpy, '-c', 'import sys; print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""} + ) + + self.assertNotIn("Consider running `python -m venv --upgrade`", proc.stderr) + @requireVenvCreate @unittest.skipUnless(can_symlink(), 'Needs symlinks') def test_sysconfig_symlinks(self): From d851e7862bbdb3285ed71c5502ab959f5f42d616 Mon Sep 17 00:00:00 2001 From: sepehrrasooli Date: Thu, 21 May 2026 15:50:51 +0330 Subject: [PATCH 7/7] Fix linting issues --- Lib/test/test_venv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index d3adfffa3766dee..b0d26b6822fb91c 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -330,7 +330,7 @@ def test_version_mismatch_warning(self): cfg_content = re.sub(r'version = \d+\.\d+', f'version = {new_version}', cfg_content) cfg_content += f'\nversion_info = {new_version}\n' - + with open(cfg_path, 'w', encoding='utf-8') as f: f.write(cfg_content) @@ -363,7 +363,7 @@ def test_version_info_mismatch_warning(self): # Add only version_info, don't modify version new_version = f"{sys.version_info.major}.{wrong_minor}" cfg_content += f'\nversion_info = {new_version}\n' - + with open(cfg_path, 'w', encoding='utf-8') as f: f.write(cfg_content)