unlock unlimited code runs for $79, limited time →
NOSSLETTER.techLIVE
back to OSSpedia
api-designbackwards-compatibility

gh-129382: Match venv API symlink defaults to the CLI

Project: python/cpython | Language: Python | Concepts: api-design, backwards-compatibility
Difficulty: mid
Link: https://github.com/python/cpython/pull/155759

The problem

The venv module's API (venv.EnvBuilder and venv.create) had a different default for the symlinks parameter than the command-line interface (python -m venv). The API defaulted to symlinks=False (copying), while the CLI used symlinks=True on non-Windows platforms and False on Windows. This inconsistency caused confusion and led to unexpected behaviors for users and tools relying on the API.

Why it's non-trivial

A naive fix might just change the default to always use symlinks, but that would break compatibility on Windows, where symlinks are less reliable or require special permissions. The fix needs to match the CLI's platform-dependent behavior without breaking existing code that explicitly sets symlinks.

The approach

1. Change the API default for symlinks from False to None and implement platform-dependent logic:

-    def __init__(self, system_site_packages=False, clear=False,
-                 symlinks=False, upgrade=False, with_pip=False, prompt=None,
+    def __init__(self, system_site_packages=False, clear=False,
+                 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

This changes the constructor to accept symlinks=None by default. If not specified, it sets self.symlinks to True on non-Windows platforms and False on Windows, matching the CLI.

2. Update the module-level create function to match:

-def create(env_dir, system_site_packages=False, clear=False,
-           symlinks=False, with_pip=False, prompt=None, upgrade_deps=False,
+def create(env_dir, system_site_packages=False, clear=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,

The default for symlinks is now None here as well, so behavior is consistent.

3. Update documentation to explain the new default:

-    * *symlinks* -- a boolean value indicating whether to attempt to symlink the
-      Python binary rather than copying.
+    * *symlinks* -- a boolean value indicating whether to attempt to symlink the
+      Python binary rather than copying. If ``None``, the default is ``False`` on
+      Windows and ``True`` on other platforms, matching the :ref:`CLI <venv-cli>`.

This tells users what happens if they don't specify symlinks.

4. Add tests to verify the new default logic:

+    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)

This test checks that the default and explicit values for symlinks behave as intended.

5. Update the changelog:

+The :class:`venv.EnvBuilder` and :func:`venv.create` APIs now use the
+platform-dependent default for symlinks from :mod:`venv`'s command-line
+interface.

Documents the change for users upgrading Python.

From the review thread

No notable review discussion on this PR.

Takeaways

  • When aligning API and CLI behavior, ensure that platform-dependent defaults are handled explicitly, not just by changing a default argument value.
  • Document all changes to default behaviors clearly in both user documentation and changelogs to avoid confusion for existing users.
  • When changing an API default, provide tests that cover both implicit (default) and explicit parameter values to prevent regressions.