gh-129382: Match venv API symlink defaults to the CLI (#155759)

hauntsaninja <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
https://github.com/python/cpython/commit/70b2b0a2dcce71e8b94757b8ed9313b0cbe0fc4f
commit: 70b2b0a2dcce71e8b94757b8ed9313b0cbe0fc4f
branch: main
author: Zanie Blue <[email protected]>
committer: hauntsaninja <[email protected]>
date: 2026-08-15T21:47:16-07:00
summary:

gh-129382: Match venv API symlink defaults to the CLI (#155759)

files:
A Misc/NEWS.d/next/Library/2025-01-31-02-27-53.gh-issue-129382.GI93CM.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 1ad2401fe2f3f31..fd9c9b9a19dd9b3 100644
--- a/Doc/library/venv.rst
+++ b/Doc/library/venv.rst
@@ -302,7 +302,7 @@ mechanisms for third-party virtual environment creators to customize environment
 creation according to their needs, the :class:`EnvBuilder` class.
 
 .. class:: EnvBuilder(system_site_packages=False, clear=False, \
-                      symlinks=False, upgrade=False, with_pip=False, \
+                      symlinks=None, upgrade=False, with_pip=False, \
                       prompt=None, upgrade_deps=False, \
                       *, scm_ignore_files=frozenset())
 
@@ -316,7 +316,8 @@ creation according to their needs, the :class:`EnvBuilder` class.
       any existing target directory, before creating the environment.
 
     * *symlinks* -- a boolean value indicating whether to attempt to symlink the
-      Python binary rather than copying.
+      Python binary rather than copying. If ``None``, the default is ``False`` on
+      Windows and ``True`` on other platforms, matching the :ref:`CLI <venv-cli>`.
 
     * *upgrade* -- a boolean value which, if true, will upgrade an existing
       environment with the running Python - for use when that Python has been
@@ -351,6 +352,9 @@ creation according to their needs, the :class:`EnvBuilder` class.
     .. versionchanged:: 3.13
        Added the ``scm_ignore_files`` parameter
 
+    .. versionchanged:: 3.16
+       The default value of *symlinks* is now platform-dependent.
+
     :class:`EnvBuilder` may be used as a base class.
 
     .. method:: create(env_dir)
@@ -521,7 +525,7 @@ creation according to their needs, the :class:`EnvBuilder` class.
 There is also a module-level convenience function:
 
 .. function:: create(env_dir, system_site_packages=False, clear=False, \
-                     symlinks=False, with_pip=False, prompt=None, \
+                     symlinks=None, with_pip=False, prompt=None, \
                      upgrade_deps=False, *, scm_ignore_files=frozenset())
 
     Create an :class:`EnvBuilder` with the given keyword arguments, and call its
@@ -541,6 +545,9 @@ There is also a module-level convenience function:
     .. versionchanged:: 3.13
        Added the *scm_ignore_files* parameter
 
+    .. versionchanged:: 3.16
+       The default value of *symlinks* is now platform-dependent.
+
 An example of extending ``EnvBuilder``
 --------------------------------------
 
diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py
index e98e52c2ea20453..b4ad1bf3f412948 100644
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -133,6 +133,31 @@ def test_defaults_with_pathlike(self):
         self.run_with_capture(venv.create, FakePath(self.env_dir))
         self._check_output_of_default_create()
 
+    def test_envbuilder_symlinks(self):
+        for kwargs, expected in (
+            ({}, os.name != 'nt'),
+            ({'symlinks': None}, os.name != 'nt'),
+            ({'symlinks': True}, True),
+            ({'symlinks': False}, False),
+        ):
+            with self.subTest(kwargs=kwargs):
+                builder = venv.EnvBuilder(**kwargs)
+                self.assertIs(builder.symlinks, expected)
+
+    def test_create_symlinks(self):
+        for kwargs, expected in (
+            ({}, os.name != 'nt'),
+            ({'symlinks': None}, os.name != 'nt'),
+            ({'symlinks': True}, True),
+            ({'symlinks': False}, False),
+        ):
+            with self.subTest(kwargs=kwargs):
+                with patch.object(venv.EnvBuilder, 'create', autospec=True) as create:
+                    venv.create(self.env_dir, **kwargs)
+                builder, env_dir = create.call_args.args
+                self.assertIs(builder.symlinks, expected)
+                self.assertEqual(env_dir, self.env_dir)
+
     def _check_output_of_default_create(self):
         self.isdir(self.bindir)
         self.isdir(self.include)
@@ -146,8 +171,7 @@ def _check_output_of_default_create(self):
         self.assertIn('home = %s' % path, data)
         self.assertIn('executable = %s' %
                       os.path.realpath(sys.executable), data)
-        copies = '' if os.name=='nt' else ' --copies'
-        cmd = (f'command = {sys.executable} -m venv{copies} --without-pip '
+        cmd = (f'command = {sys.executable} -m venv --without-pip '
                f'--without-scm-ignore-files {self.env_dir}')
         self.assertIn(cmd, data)
         fn = self.get_env_file(self.bindir, self.exe)
@@ -156,6 +180,7 @@ def _check_output_of_default_create(self):
             print('Contents of %r:' % bd)
             print('    %r' % os.listdir(bd))
         self.assertTrue(os.path.exists(fn), 'File %r should exist.' % fn)
+        self.assertEqual(os.path.islink(fn), os.name != 'nt' and can_symlink())
 
     def test_config_file_command_key(self):
         options = [
diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py
index bd2762d55ef6961..4c8e4e8efeaa724 100644
--- a/Lib/venv/__init__.py
+++ b/Lib/venv/__init__.py
@@ -27,16 +27,15 @@ class EnvBuilder:
     By default, the builder makes the system (global) site-packages dir
     *un*available to the created environment.
 
-    If invoked using the Python -m option, the default is to use copying
-    on Windows platforms but symlinks elsewhere. If instantiated some
-    other way, the default is to *not* use symlinks.
+    By default, the builder uses copying on Windows platforms but symlinks
+    elsewhere, matching the behaviour when invoked using the Python -m option.
 
     :param system_site_packages: If True, the system (global) site-packages
                                  dir is available to created environments.
     :param clear: If True, delete the contents of the environment directory if
                   it already exists, before environment creation.
     :param symlinks: If True, attempt to symlink rather than copy files into
-                     virtual environment.
+                     virtual environment. If None, use the platform default.
     :param upgrade: If True, upgrade an existing virtual environment.
     :param with_pip: If True, ensure pip is installed in the virtual
                      environment
@@ -47,11 +46,11 @@ class EnvBuilder:
     """
 
     def __init__(self, system_site_packages=False, clear=False,
-                 symlinks=False, upgrade=False, with_pip=False, prompt=None,
+                 symlinks=None, upgrade=False, with_pip=False, prompt=None,
                  upgrade_deps=False, *, scm_ignore_files=frozenset()):
         self.system_site_packages = system_site_packages
         self.clear = clear
-        self.symlinks = symlinks
+        self.symlinks = os.name != 'nt' if symlinks is None else symlinks
         self.upgrade = upgrade
         self.with_pip = with_pip
         self.orig_prompt = prompt
@@ -607,7 +606,7 @@ def upgrade_dependencies(self, context):
 
 
 def create(env_dir, system_site_packages=False, clear=False,
-           symlinks=False, with_pip=False, prompt=None, upgrade_deps=False,
+           symlinks=None, with_pip=False, prompt=None, upgrade_deps=False,
            *, scm_ignore_files=frozenset()):
     """Create a virtual environment in a directory."""
     builder = EnvBuilder(system_site_packages=system_site_packages,
diff --git a/Misc/NEWS.d/next/Library/2025-01-31-02-27-53.gh-issue-129382.GI93CM.rst b/Misc/NEWS.d/next/Library/2025-01-31-02-27-53.gh-issue-129382.GI93CM.rst
new file mode 100644
index 000000000000000..b1b704cc6f86537
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-01-31-02-27-53.gh-issue-129382.GI93CM.rst
@@ -0,0 +1,3 @@
+The :class:`venv.EnvBuilder` and :func:`venv.create` APIs now use the
+platform-dependent default for symlinks from :mod:`venv`'s command-line
+interface.

_______________________________________________
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.