From f53f6657b6aff0411b5346931033a5a3a92752a6 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Tue, 18 Aug 2026 13:41:09 +0300 Subject: [PATCH] gh-155997: Fix list_all() if an interpreter is destroyed during the call Creating the Interpreter objects can start a garbage collection which finalizes an object owning the last reference to a listed interpreter. Skip interpreters which no longer exist instead of failing. --- Lib/concurrent/interpreters/__init__.py | 10 ++++++++-- Lib/test/test_interpreters/test_api.py | 15 +++++++++++++++ ...2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst | 3 +++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst diff --git a/Lib/concurrent/interpreters/__init__.py b/Lib/concurrent/interpreters/__init__.py index ea4147ee9a25da..335a744b727d10 100644 --- a/Lib/concurrent/interpreters/__init__.py +++ b/Lib/concurrent/interpreters/__init__.py @@ -68,8 +68,14 @@ def create(): def list_all(): """Return all existing interpreters.""" - return [Interpreter(id, _whence=whence) - for id, whence in _interpreters.list_all(require_ready=True)] + interps = [] + for id, whence in _interpreters.list_all(require_ready=True): + try: + interps.append(Interpreter(id, _whence=whence)) + except InterpreterNotFoundError: + # It was destroyed after it was listed. + pass + return interps def get_current(): diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py index 13d23af5aceb47..aac3cdd717668c 100644 --- a/Lib/test/test_interpreters/test_api.py +++ b/Lib/test/test_interpreters/test_api.py @@ -289,6 +289,21 @@ def test_idempotent(self): for interp1, interp2 in zip(actual, expected): self.assertIs(interp1, interp2) + def test_destroyed_by_gc(self): + # gh-155997: the interpreter is destroyed while list_all() runs. + interp = interpreters.create() + interpid = interp.id + cycle = [] + cycle.append(cycle) + cycle.append(interp) + # The cycle holds the only reference, so only the collector frees it. + with support.disable_gc(): + del interp, cycle + + with support.gc_threshold(1): + ids = [i.id for i in interpreters.list_all()] + self.assertNotIn(interpid, ids) + def test_created_with_capi(self): mainid, *_ = _interpreters.get_main() interpid1 = _interpreters.create() diff --git a/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst b/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst new file mode 100644 index 00000000000000..2727ba34aad8ff --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-18-13-41-00.gh-issue-155997.CjbD1F.rst @@ -0,0 +1,3 @@ +Fix :func:`concurrent.interpreters.list_all`. It failed if an interpreter +was destroyed during the call, in particular by a garbage collection which +finalized the object owning the last reference to it.