https://github.com/python/cpython/commit/71cb9e8b64c4a89971955d3ce8ff393f283aeace
commit: 71cb9e8b64c4a89971955d3ce8ff393f283aeace
branch: main
author: Clay Dugo <[email protected]>
committer: brettcannon <[email protected]>
date: 2026-08-28T23:37:42Z
summary:

gh-143768: Replace a dangling interpreter symlink when creating a venv (#150985)

Co-authored-by: Brett Cannon <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst
M Doc/library/venv.rst
M Lib/test/test_venv.py
M Lib/venv/__init__.py

diff --git a/Doc/library/venv.rst b/Doc/library/venv.rst
index fd9c9b9a19dd9b3..34b21aba17461b4 100644
--- a/Doc/library/venv.rst
+++ b/Doc/library/venv.rst
@@ -452,6 +452,12 @@ creation according to their needs, the :class:`EnvBuilder` 
class.
         On POSIX systems, if a specific executable ``python3.x`` was used,
         symlinks to ``python`` and ``python3`` will be created pointing to that
         executable, unless files with those names already exist.
+        On POSIX systems, a broken symlink at a destination path is removed
+        before the copy or symlink is created.
+
+        .. versionchanged:: next
+           A broken symlink at a destination path is now removed and replaced.
+           Previously it was left in place, or it made the copy fail.
 
     .. method:: setup_scripts(context)
 
diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
index b4ad1bf3f412948..2f30d3108021dc6 100644
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -946,6 +946,33 @@ def test_failed_symlink(self):
             filepath_regex = r"'[A-Z]:\\\\(?:[^\\\\]+\\\\)*[^\\\\]+'"
             self.assertRegex(err, rf"Unable to symlink {filepath_regex} to 
{filepath_regex}")
 
+    @requireVenvCreate
+    @unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
+    @unittest.skipUnless(can_symlink(), 'Needs symlinks')
+    def test_broken_symlink_in_existing_venv(self):
+        """
+        Test creating a venv when a stale venv with broken symlinks exists.
+        """
+        bindir = os.path.join(self.env_dir, self.bindir)
+        os.makedirs(bindir)
+        python = os.path.join(bindir, 'python3')
+        os.symlink('/path/to/deleted/env/bin/python3', python)
+        self.assertTrue(os.path.islink(python))
+        self.assertFalse(os.path.exists(python))
+
+        builder = venv.EnvBuilder(with_pip=False, symlinks=True)
+        self.run_with_capture(builder.create, self.env_dir)
+        self.assertTrue(os.path.islink(python))
+        self.assertTrue(os.path.exists(python))
+
+        rmtree(self.env_dir)
+        os.makedirs(bindir)
+        os.symlink('/path/to/deleted/env/bin/python3', python)
+        builder = venv.EnvBuilder(with_pip=False, symlinks=False)
+        self.run_with_capture(builder.create, self.env_dir)
+        self.assertFalse(os.path.islink(python))
+        self.assertTrue(os.path.exists(python))
+
     @requireVenvCreate
     def test_multiprocessing(self):
         """
diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py
index 4c8e4e8efeaa724..38e1bfe0c5fdb9d 100644
--- a/Lib/venv/__init__.py
+++ b/Lib/venv/__init__.py
@@ -266,6 +266,8 @@ def symlink_or_copy(self, src, dst, 
relative_symlinks_ok=False):
         switch to a different set of files instead.)
         """
         assert os.name != 'nt'
+        if os.path.islink(dst) and not os.path.exists(dst):
+            os.unlink(dst)
         force_copy = not self.symlinks
         if not force_copy:
             try:
diff --git 
a/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst 
b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst
new file mode 100644
index 000000000000000..1aacaf432ccfb25
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-06-05-16-57-03.gh-issue-143768.RbLnkFx.rst
@@ -0,0 +1,3 @@
+:mod:`venv`: Replace a dangling interpreter symlink in an existing
+virtual environment instead of failing or silently leaving it broken.
+Fix by Clay Dugo.

_______________________________________________
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]

Reply via email to