From c8bfc1d5140d39654e1dd92ee4a6a9e66f9e1758 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 21 Jul 2026 17:24:01 +0200 Subject: [PATCH 1/4] PEP 383: Add `python-version` to `pyvenv.cfg` Draft PR accompanying [PEP 838](https://peps.python.org/pep-0838/) --- Lib/test/test_venv.py | 3 +++ Lib/venv/__init__.py | 2 ++ 2 files changed, 5 insertions(+) diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 58ae85fb268042e..e7d5e890c1c31a4 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -146,6 +146,9 @@ def _check_output_of_default_create(self): self.assertIn('home = %s' % path, data) self.assertIn('executable = %s' % os.path.realpath(sys.executable), data) + self.assertIn("python-version = %d.%d" % (sys.version_info.major, + sys.version_info.minor), + data) copies = '' if os.name=='nt' else ' --copies' cmd = (f'command = {sys.executable} -m venv{copies} --without-pip ' f'--without-scm-ignore-files {self.env_dir}') diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index bd2762d55ef6961..0a96d11cb9baa3e 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -232,6 +232,8 @@ def create_configuration(self, context): incl = 'false' f.write('include-system-site-packages = %s\n' % incl) f.write('version = %d.%d.%d\n' % sys.version_info[:3]) + f.write('python-version = %d.%d\n' % (sys.version_info.major, + sys.version_info.minor)) if self.prompt is not None: f.write(f'prompt = {self.prompt!r}\n') f.write('executable = %s\n' % os.path.realpath(sys.executable)) From 2856a2ecac18d7cb16534c8fb434161440be430e Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 21 Jul 2026 17:30:12 +0200 Subject: [PATCH 2/4] A newsfragment --- .../next/Library/2026-07-21-17-29-19.gh-issue-154377.pep838.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-21-17-29-19.gh-issue-154377.pep838.rst diff --git a/Misc/NEWS.d/next/Library/2026-07-21-17-29-19.gh-issue-154377.pep838.rst b/Misc/NEWS.d/next/Library/2026-07-21-17-29-19.gh-issue-154377.pep838.rst new file mode 100644 index 000000000000000..e760a6f436bd11f --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-21-17-29-19.gh-issue-154377.pep838.rst @@ -0,0 +1,2 @@ +Add a ``python-version`` key to ``pyvenv.cfg`` files created by :mod:`venv`. +This implements :pep:`838`. From 1b2f494b96ded56b5f7920f58d4f7687bd28be0f Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 21 Jul 2026 17:54:46 +0200 Subject: [PATCH 3/4] PEP 838: Reject python startup with mismatched python-version This is a stronger version of https://github.com/python/cpython/issues/127727 / https://github.com/python/cpython/pull/149715: Using the standardized `python-version`, we fail at startup with a helpful error message instead of running up until an unclear `ModuleNotFoundError` because the shared library has a tag for a different Python version. This change checks the major version, we shouldn't silently pass when encountering a potential Python 4 that mismatches. This is an optional part of the PEP and may equally be a warning as it currently is with `version`/`version_info`. Stacked on top of https://github.com/python/cpython/pull/154378. --- Lib/site.py | 65 ++++++++++++------- Lib/test/test_venv.py | 47 ++++++++++++-- ...-07-21-17-53-52.gh-issue-154377.venver.rst | 2 + 3 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-07-21-17-53-52.gh-issue-154377.venver.rst diff --git a/Lib/site.py b/Lib/site.py index 873c562d890a3bb..5a5fb83f1206556 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -982,7 +982,7 @@ def _venv(state): if candidate_conf: virtual_conf = candidate_conf system_site = "true" - version, version_info = None, None + version, version_info, python_version = None, 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: @@ -999,31 +999,46 @@ def _venv(state): version = value elif key == 'version_info': version_info = value - - for field_name, field_value in [ - ('version',version), ('version_info',version_info) + elif key == 'python-version': + python_version = value + + for field_name, field_value, should_error in [ + # Run the fatal check first. + ('python_version', python_version, True), + ('version',version, False), + ('version_info',version_info, False), ]: - 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 field_value is None: + continue + 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, + ) + continue + if ( + major == sys.version_info.major + and minor == sys.version_info.minor + ): + continue + if should_error: + raise RuntimeError( + 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.", + ) + else: + _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 e7d5e890c1c31a4..093ac45fba61f47 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -313,6 +313,45 @@ def test_sysconfig(self): out, err = check_output(cmd, encoding='utf-8') self.assertEqual(out.strip(), expected, err) + @requireVenvCreate + def test_python_version_mismatch_error(self): + rmtree(self.env_dir) + self.run_with_capture(venv.create, self.env_dir, with_pip=False) + + corrct_version = f"{sys.version_info.major}.{sys.version_info.minor}" + wrong_version = f"{sys.version_info.major}.{sys.version_info.minor + 1}" + + cfg_path = self.get_env_file("pyvenv.cfg") + with open(cfg_path, encoding="utf-8") as f: + cfg_content = f.read() + + cfg_content = cfg_content.replace( + f"python-version = {corrct_version}", + f"python-version = {wrong_version}", + ) + + 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", 'print("done")'], + capture_output=True, + text=True, + env={**os.environ, "PYTHONHOME": ""}, + ) + + self.assertNotEqual(proc.returncode, 0) + self.assertNotIn("done", proc.stdout) + self.assertIn("RuntimeError", proc.stderr) + self.assertIn(f"Python {wrong_version}", proc.stderr) + self.assertIn( + f"Python {sys.version_info.major}.{sys.version_info.minor}", + proc.stderr, + ) + self.assertIn("Consider running `python -m venv --upgrade`", proc.stderr) + @requireVenvCreate def test_version_mismatch_warning(self): """ @@ -330,7 +369,7 @@ def test_version_mismatch_warning(self): 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 = re.sub(r'(?m)^version = \d+\.\d+', f'version = {new_version}', cfg_content) cfg_content += f'\nversion_info = {new_version}\n' @@ -422,7 +461,7 @@ def test_malformed_version_warning(self): malformed_version = "not.a.version" if 'version =' in cfg_content: - cfg_content = re.sub(r'version = .+', f'version = {malformed_version}', cfg_content) + cfg_content = re.sub(r'(?m)^version = .+', f'version = {malformed_version}', cfg_content) with open(cfg_path, 'w', encoding='utf-8') as f: f.write(cfg_content) @@ -483,7 +522,7 @@ def test_conflicting_version_fields(self): 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) + cfg_content = re.sub(r'(?m)^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' @@ -519,7 +558,7 @@ def test_different_major_version_no_warning(self): 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) + cfg_content = re.sub(r'(?m)^version = \d+\.\d+', f'version = {new_version}', cfg_content) with open(cfg_path, 'w', encoding='utf-8') as f: f.write(cfg_content) diff --git a/Misc/NEWS.d/next/Library/2026-07-21-17-53-52.gh-issue-154377.venver.rst b/Misc/NEWS.d/next/Library/2026-07-21-17-53-52.gh-issue-154377.venver.rst new file mode 100644 index 000000000000000..3a811bcd4781df7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-21-17-53-52.gh-issue-154377.venver.rst @@ -0,0 +1,2 @@ +Fail at startup when the ``python-version`` in a virtual +environment's ``pyvenv.cfg`` does not match the running interpreter. From fd185cbeab2a9d51ec24b8bf3b0a2f0501ed7070 Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 21 Jul 2026 18:36:22 +0200 Subject: [PATCH 4/4] gh-154377: Preserve legacy venv version warning behavior --- Lib/site.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/Lib/site.py b/Lib/site.py index 5a5fb83f1206556..ff68f1fcef87201 100644 --- a/Lib/site.py +++ b/Lib/site.py @@ -1018,19 +1018,18 @@ def _venv(state): RuntimeWarning, ) continue - if ( + if should_error: + if (major, minor) != sys.version_info[:2]: + raise RuntimeError( + 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.", + ) + elif ( major == sys.version_info.major - and minor == sys.version_info.minor + and minor != sys.version_info.minor ): - continue - if should_error: - raise RuntimeError( - 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.", - ) - else: _warn( f"This virtual environment was created for Python {major}.{minor}, " f"but the current interpreter is Python "