https://github.com/python/cpython/commit/3c298e2e385fc6f462abaada2fd680deb1a2b58e
commit: 3c298e2e385fc6f462abaada2fd680deb1a2b58e
branch: main
author: Barry Warsaw <[email protected]>
committer: warsaw <[email protected]>
date: 2026-05-21T21:44:13Z
summary:

gh-149819: fix .pth and .start file processing in subprocess when inheriting 
PYTHONPATH (#150177)

* gh-149819: Fix .pth files not loaded in Python subprocesses

After PR gh-149583 (Fix double evaluation of .pth and .site files in
venvs), .pth files are no longer loaded in subprocesses started with
subprocess.run([sys.executable, ...]).  The root cause: main() seeds
known_paths from removeduppaths() with all sys.path entries inherited
from the parent process.  addsitedir() then skips .pth processing for
every directory already in known_paths.

Fix:
- main(): call removeduppaths() for dedup but start known_paths as a
  fresh empty set, so that addsitedir() processes .pth files in every
  site-packages directory regardless of inherited sys.path.
- addsitedir(): move known_paths.add() before the sys.path.append and
  guard the append with 'sitedir not in sys.path' to avoid creating
  duplicate entries when called with a fresh known_paths.

This preserves the gh-75723 dedup guarantee while allowing subprocesses
to load .pth files.

* Fill out the tests for GH#149888

* Extend _make_start() and _make_pth() to take an optional `basedir` which is 
used instead of
 `site.tmpdir` if given.
* Add test_pth_processed_when_sitedir_already_on_path() to test the core 
GH#149819 bug: .pth files
  in subprocesses aren't handled if PYTHONPATH pointing to the .pth directory 
is inherited.
* Similarly add test_start_processed_when_sitedir_already_on_path() to verify 
that .start files in
  the same circumstances are also now processed.

* Update Lib/site.py

Co-authored-by: scoder <[email protected]>

* Oops!  Remove redundant code

---------

Co-authored-by: BugBounty Mind <[email protected]>
Co-authored-by: scoder <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-05-15-16-28-00.gh-issue-149819.fixpth.rst
M Lib/site.py
M Lib/test/test_site.py

diff --git a/Lib/site.py b/Lib/site.py
index 64e8192a9ac81a..239ee0d6f57bce 100644
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -490,13 +490,16 @@ def addsitedir(sitedir, known_paths=None, *, 
defer_processing_start_files=False)
         reset = False
     sitedir, sitedircase = makepath(sitedir)
 
-    # If the normcase'd new sitedir isn't already known, append it to
-    # sys.path, keep a record of it, and process all .pth and .start files
-    # found in that directory.  If the new sitedir is known, be sure not
-    # to process all of those more than once!  gh-75723
+    # If the normcase'd new sitedir isn't already known, record it to
+    # prevent re-processing, append it to sys.path (only if not already
+    # present), and process all .pth and .start files found in that
+    # directory.  Use a direct sys.path membership check for the append
+    # guard so that callers (like main()) can pass a fresh known_paths
+    # set while avoiding duplicate sys.path entries (gh-149819).
     if sitedircase not in known_paths:
-        sys.path.append(sitedir)
         known_paths.add(sitedircase)
+        if sitedir not in sys.path:
+            sys.path.append(sitedir)
 
         try:
             names = os.listdir(sitedir)
@@ -1000,13 +1003,13 @@ def main():
     global ENABLE_USER_SITE
 
     orig_path = sys.path[:]
-    known_paths = removeduppaths()
+    removeduppaths()
     if orig_path != sys.path:
         # removeduppaths() might make sys.path absolute.
         # Fix __file__ of already imported modules too.
         abs_paths()
 
-    known_paths = venv(known_paths)
+    known_paths = venv(known_paths=set())
     if ENABLE_USER_SITE is None:
         ENABLE_USER_SITE = check_enableusersite()
     known_paths = addusersitepackages(known_paths, 
defer_processing_start_files=True)
diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py
index 0e6f352f49cd38..e2a81b82321ede 100644
--- a/Lib/test/test_site.py
+++ b/Lib/test/test_site.py
@@ -456,6 +456,7 @@ def cleanup(self, prep=False):
         if os.path.exists(self.bad_dir_path):
             os.rmdir(self.bad_dir_path)
 
+
 class ImportSideEffectTests(unittest.TestCase):
     """Test side-effects from importing 'site'."""
 
@@ -545,7 +546,6 @@ def test_customization_modules_on_startup(self):
                     output = subprocess.check_output([sys.executable, '-s', 
'-c', '""'])
                     self.assertNotIn(eyecatcher, output.decode('utf-8'))
 
-
     @unittest.skipUnless(hasattr(urllib.request, "HTTPSHandler"),
                          'need SSL support to download license')
     @test.support.requires_resource('network')
@@ -926,18 +926,28 @@ def setUp(self):
     def _reset_startup_state(self):
         site._startup_state = None
 
-    def _make_start(self, content, name='testpkg'):
-        """Write a <name>.start file and return its basename."""
+    def _make_start(self, content, name='testpkg', basedir=None):
+        """Write a <name>.start file and return its basename.
+
+        ``basedir`` defaults to ``self.tmpdir``.  Pass an explicit directory
+        when the .start file needs to live somewhere other than the test's
+        primary tmpdir (e.g. a nested user-site).
+        """
         basename = f"{name}.start"
-        filepath = os.path.join(self.tmpdir, basename)
+        filepath = os.path.join(self.tmpdir if basedir is None else basedir, 
basename)
         with open(filepath, 'w', encoding='utf-8') as f:
             f.write(content)
         return basename
 
-    def _make_pth(self, content, name='testpkg'):
-        """Write a <name>.pth file and return its basename."""
+    def _make_pth(self, content, name='testpkg', basedir=None):
+        """Write a <name>.pth file and return its basename.
+
+        ``basedir`` defaults to ``self.tmpdir``.  Pass an explicit directory
+        when the .pth file needs to live somewhere other than the test's
+        primary tmpdir (e.g. a nested user-site).
+        """
         basename = f"{name}.pth"
-        filepath = os.path.join(self.tmpdir, basename)
+        filepath = os.path.join(self.tmpdir if basedir is None else basedir, 
basename)
         with open(filepath, 'w', encoding='utf-8') as f:
             f.write(content)
         return basename
@@ -1640,6 +1650,80 @@ def bootstrap():
         self.assertIn(overlay, sys.path)
         self.assertIn(pkgdir, sys.path)
 
+    # gh-149819
+    @unittest.skipUnless(site.ENABLE_USER_SITE, "requires user-site")
+    @support.requires_subprocess()
+    def test_pth_processed_when_sitedir_already_on_path(self):
+        # A .pth file in a site-packages directory must still be processed by
+        # site.main() when that directory is already on sys.path at
+        # interpreter start up, for example in a subprocess that inherits
+        # PYTHONPATH from its parent.  Before the fix, main() seeded
+        # known_paths with all entries derived from removeduppaths(), and
+        # addsitedir() then skipped .pth processing for any directory already
+        # in known_paths.
+        user_base = self.tmpdir
+        user_site = site._get_path(user_base)
+        os.makedirs(user_site)
+        sentinel = "GH149819_PTH_RAN"
+        # Writing some text to stderr is the simplest observable side effect.
+        self._make_pth(f"""\
+import sys; sys.stderr.write({sentinel!r}); sys.stderr.flush()
+""",
+            name='gh149819',
+            basedir=user_site)
+        with EnvironmentVarGuard() as env:
+            # PYTHONUSERBASE points USER_SITE at our temp directory so
+            # site.main() will call addsitedir() on it, rather than on the
+            # host interpreter's real user-site.
+            env['PYTHONUSERBASE'] = user_base
+            # PYTHONPATH puts that same directory on sys.path before
+            # site.main() runs in the subprocess.  This is what triggers the
+            # bug: removeduppaths() records it in known_paths, and the unfixed
+            # addsitedir() then skips .pth processing.
+            env['PYTHONPATH'] = user_site
+            result = subprocess.run(
+                [sys.executable, '-c', ''],
+                capture_output=True,
+                check=True,
+            )
+        self.assertIn(sentinel.encode(), result.stderr)
+
+    @unittest.skipUnless(site.ENABLE_USER_SITE, "requires user-site")
+    @support.requires_subprocess()
+    def test_start_processed_when_sitedir_already_on_path(self):
+        # Companion to test_pth_processed_when_sitedir_already_on_path:
+        # the same dedup-guard skip in addsitedir() suppressed both .pth
+        # and .start file processing, so verify .start entry points also
+        # run for a site-packages directory inherited via PYTHONPATH.
+        user_base = self.tmpdir
+        user_site = site._get_path(user_base)
+        os.makedirs(user_site)
+        sentinel = "GH149819_START_RAN"
+        # The .start entry point resolves to a callable, so we write a
+        # tiny importable module that outputs the sentinel text.  It lands in
+        # <self.sitedir>/extdir.  That path is added to PYTHONPATH below so
+        # the subprocess can import it.
+        extdir = self._make_mod(f"""\
+import sys
+def run():
+    sys.stderr.write({sentinel!r})
+    sys.stderr.flush()
+""", name='gh149819mod')
+        self._make_start(
+            'gh149819mod:run\n', name='gh149819', basedir=user_site
+        )
+        with EnvironmentVarGuard() as env:
+            # See above for details.
+            env['PYTHONUSERBASE'] = user_base
+            env['PYTHONPATH'] = os.pathsep.join([user_site, extdir])
+            result = subprocess.run(
+                [sys.executable, '-c', ''],
+                capture_output=True,
+                check=True,
+            )
+        self.assertIn(sentinel.encode(), result.stderr)
+
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git 
a/Misc/NEWS.d/next/Library/2026-05-15-16-28-00.gh-issue-149819.fixpth.rst 
b/Misc/NEWS.d/next/Library/2026-05-15-16-28-00.gh-issue-149819.fixpth.rst
new file mode 100644
index 00000000000000..66e6da0ecf0d87
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-05-15-16-28-00.gh-issue-149819.fixpth.rst
@@ -0,0 +1,4 @@
+Fix regression in :func:`site.addsitedir` where ``.pth`` files were no
+longer processed in Python subprocesses. This happened because
+:func:`site.main` seeded ``known_paths`` with entries inherited from
+the parent process, causing ``addsitedir`` to skip ``.pth`` processing.

_______________________________________________
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