https://github.com/python/cpython/commit/afda368fd6ab62ecaea053f9d32b62aa83cfab3f
commit: afda368fd6ab62ecaea053f9d32b62aa83cfab3f
branch: main
author: Sepehr Rasouli <[email protected]>
committer: FFY00 <[email protected]>
date: 2026-06-22T15:29:15+01:00
summary:
gh-127727: Warn when running a virtual environment created for a different
minor Python version (#149715)
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-05-12-12-49-30.gh-issue-127727.hNmj9G.rst
M Lib/site.py
M Lib/test/test_venv.py
diff --git a/Lib/site.py b/Lib/site.py
index d06549b8df800e..873c562d890a3b 100644
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -982,6 +982,7 @@ def _venv(state):
if candidate_conf:
virtual_conf = candidate_conf
system_site = "true"
+ 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:
@@ -994,6 +995,35 @@ def _venv(state):
system_site = value.lower()
elif key == 'home':
sys._home = value
+ elif key == 'version':
+ version = value
+ 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(
diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
index 0c6cd1b196ac81..8075a1947918be 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):
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 00000000000000..474ae434f6885e
--- /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``.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]