From 850a898b89f422a65a1bafc3ba51c2323c7fd507 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Wed, 1 Oct 2025 16:15:49 +0200 Subject: [PATCH 1/8] Make environ.clear() asymptotically efficient (O(N^2) -> O(N)). The implicit implementation of environ.clear() is MutableMapping.clear(), which calls iter(self) in a loop, once per deleted key. But because iter(self) for environ creates a snapshot of all keys, this results in O(N^2) complexity for environ.clear(). This problem is especially evident on large environments. On my M3 MacBook Pro, it takes 500ms to clear an environment with only 10K variables. A more extreme example: 100K variables take 23s to clear. Environments with thousands of environment variables are rare, but they do exist. The new implementation avoids creating a snapshot of the keys on each iteration, and instead repeatedly tries to delete keys until the environment is empty. This mirrors the current behavior of environ.clear(), while being more efficient asymptotically. Further improvement on Linux/FreeBSD could be achieved by using clearenv() which is part of the standard C library. --- Lib/os.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Lib/os.py b/Lib/os.py index 710d6f8cfcdf74..550bd1e006ca80 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -742,6 +742,14 @@ def __repr__(self): ) return f"environ({{{formatted_items}}})" + def clear(self): + while self._data: + for key in list(self._data): + try: + del self[key] + except KeyError: + pass + def copy(self): return dict(self) From 7248570981464794f70d78c219265c19d3071387 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Wed, 1 Oct 2025 17:47:07 +0200 Subject: [PATCH 2/8] Add NEWS entry --- .../next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst diff --git a/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst b/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst new file mode 100644 index 00000000000000..d7b5a264bb91ee --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst @@ -0,0 +1,2 @@ +Make os.environ.clear() more efficient (linear complexity instead of quadratic) + From 67cc14063328df2b6a1270b4c96a5b9c8bd27261 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Wed, 1 Oct 2025 17:54:22 +0200 Subject: [PATCH 3/8] Account for key encoding --- Lib/os.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/os.py b/Lib/os.py index 550bd1e006ca80..97e53a907e9201 100644 --- a/Lib/os.py +++ b/Lib/os.py @@ -745,8 +745,9 @@ def __repr__(self): def clear(self): while self._data: for key in list(self._data): + unsetenv(key) try: - del self[key] + del self._data[key] except KeyError: pass From 8c1ca2099b1bd7b7fb36dfbb0133cc2e36ac9e2c Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Wed, 1 Oct 2025 18:09:27 +0200 Subject: [PATCH 4/8] Update Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- .../Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst b/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst index d7b5a264bb91ee..754e5c8c9318f1 100644 --- a/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst +++ b/Misc/NEWS.d/next/Library/2025-10-01-17-45-27.gh-issue-139482.yDMeEa.rst @@ -1,2 +1,2 @@ -Make os.environ.clear() more efficient (linear complexity instead of quadratic) - +Ensure that :data:`os.environ.clear() ` +has linear complexity instead of quadratic complexity. From e116604df7248f12b5fae8b78f7179a9b130eaee Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Thu, 2 Oct 2025 20:27:57 +0200 Subject: [PATCH 5/8] Add unit tests --- Lib/test/test_os.py | 70 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index 1180e27a7a5310..b539854b82e5b6 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1504,6 +1504,76 @@ def test_reload_environ(self): self.assertNotIn(b'test_env', os.environb) self.assertNotIn('test_env', os.environ) + def test_clear_empties_environ(self): + os.environ["test_key_to_clear1"] = "test_value_to_clear1" + os.environ["test_key_to_clear2"] = "test_value_to_clear2" + os.environ["test_key_to_clear3"] = "test_value_to_clear3" + + # test the environ.clear() removes the + os.environ.clear() + + self.assertEqual(os.environ, {}) + if os.supports_bytes_environ: + self.assertEqual(os.environb, {}) + + # repeated calls should be idempotent + os.environ.clear() + + self.assertEqual(os.environ, {}) + if os.supports_bytes_environ: + self.assertEqual(os.environb, {}) + + @unittest.skipUnless(os.supports_bytes_environ, "os.environb required for this test.") + def test_clear_empties_environb(self): + os.environb[b"test_key_to_clear1"] = b"test_value_to_clear1" + os.environb[b"test_key_to_clear2"] = b"test_value_to_clear2" + os.environb[b"test_key_to_clear3"] = b"test_value_to_clear3" + + # test the environ.clear() removes the + os.environb.clear() + + self.assertEqual(os.environ, {}) + self.assertEqual(os.environb, {}) + + # repeated calls should be idempotent + os.environb.clear() + + self.assertEqual(os.environ, {}) + self.assertEqual(os.environb, {}) + + def test_clear_empties_process_environment(self): + # Determine if on the current platform os.unsetenv() + # updates process environment. + os.environ['to_remove'] = 'value' + os.unsetenv('to_remove') + os.reload_environ() + if 'to_remove' in os.environ: + self.skipTest("os.unsetenv() doesn't update the process environment on this platform.") + + # Set up two environment variables to be cleared + os.environ["test_env1"] = "some_value1" + os.environ["test_env2"] = "some_value2" + + # Ensure the variables were persisted on process level. + os.reload_environ() + self.assertEqual(os.getenv("test_env1"), "some_value1") + self.assertEqual(os.getenv("test_env2"), "some_value2") + + # Test that os.clear() clears both os.environ and os.environb + os.environ.clear() + + self.assertEqual(os.environ, {}) + if os.supports_bytes_environ: + self.assertEqual(os.environb, {}) + + # Test that os.clear() also clears the process environment, + # so that after os.reload_environ() environ and environb are still empty. + os.reload_environ() + self.assertEqual(os.environ, {}) + if os.supports_bytes_environ: + self.assertEqual(os.environb, {}) + + class WalkTests(unittest.TestCase): """Tests for os.walk().""" is_fwalk = False From 56718b4625a1ba14f34abbc3d579ec4903355b19 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Thu, 2 Oct 2025 20:29:09 +0200 Subject: [PATCH 6/8] Fix comments --- Lib/test/test_os.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py index b539854b82e5b6..f813d57734b646 100644 --- a/Lib/test/test_os.py +++ b/Lib/test/test_os.py @@ -1509,14 +1509,14 @@ def test_clear_empties_environ(self): os.environ["test_key_to_clear2"] = "test_value_to_clear2" os.environ["test_key_to_clear3"] = "test_value_to_clear3" - # test the environ.clear() removes the + # Test environ.clear() removes the environment variables os.environ.clear() self.assertEqual(os.environ, {}) if os.supports_bytes_environ: self.assertEqual(os.environb, {}) - # repeated calls should be idempotent + # Repeated calls should be idempotent os.environ.clear() self.assertEqual(os.environ, {}) @@ -1529,13 +1529,13 @@ def test_clear_empties_environb(self): os.environb[b"test_key_to_clear2"] = b"test_value_to_clear2" os.environb[b"test_key_to_clear3"] = b"test_value_to_clear3" - # test the environ.clear() removes the + # Test environ.clear() removes the environment variables os.environb.clear() self.assertEqual(os.environ, {}) self.assertEqual(os.environb, {}) - # repeated calls should be idempotent + # Repeated calls should be idempotent os.environb.clear() self.assertEqual(os.environ, {}) From c3390e36d01f15469f8ccb63aaeb2c9b8efa7ea7 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Thu, 2 Oct 2025 20:35:55 +0200 Subject: [PATCH 7/8] remote trailing whitespace --- Lib/test/test_os/test_os.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 21dc94846b23bb..16b424d1c82466 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -1548,7 +1548,7 @@ def test_clear_empties_process_environment(self): os.reload_environ() self.assertEqual(os.getenv("test_env1"), "some_value1") self.assertEqual(os.getenv("test_env2"), "some_value2") - + # Test that os.clear() clears both os.environ and os.environb os.environ.clear() From b348afab989eb2a827b6c1f241a80254f41ae50f Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Sat, 11 Oct 2025 10:34:01 +0200 Subject: [PATCH 8/8] remove newlines --- Lib/test/test_os/test_os.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Lib/test/test_os/test_os.py b/Lib/test/test_os/test_os.py index 16b424d1c82466..fd08c9a000e2cb 100644 --- a/Lib/test/test_os/test_os.py +++ b/Lib/test/test_os/test_os.py @@ -1501,14 +1501,12 @@ def test_clear_empties_environ(self): # Test environ.clear() removes the environment variables os.environ.clear() - self.assertEqual(os.environ, {}) if os.supports_bytes_environ: self.assertEqual(os.environb, {}) # Repeated calls should be idempotent os.environ.clear() - self.assertEqual(os.environ, {}) if os.supports_bytes_environ: self.assertEqual(os.environb, {}) @@ -1521,13 +1519,11 @@ def test_clear_empties_environb(self): # Test environ.clear() removes the environment variables os.environb.clear() - self.assertEqual(os.environ, {}) self.assertEqual(os.environb, {}) # Repeated calls should be idempotent os.environb.clear() - self.assertEqual(os.environ, {}) self.assertEqual(os.environb, {}) @@ -1551,7 +1547,6 @@ def test_clear_empties_process_environment(self): # Test that os.clear() clears both os.environ and os.environb os.environ.clear() - self.assertEqual(os.environ, {}) if os.supports_bytes_environ: self.assertEqual(os.environb, {})