From 76b4d222590c0ab9b9b8caa67fa0b0d6401d3239 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 16 Oct 2026 13:23:58 -0700 Subject: [PATCH 1/4] Free the deep lazy import chain on the main thread in test_lazy_import test_deeply_dotted_name() builds a 10,000-deep chain of lazy import placeholders in a thread with a 256 KiB C stack. Freeing the chain there defers most deallocations to the trashcan, which only drains them once the stack margin recovers. With a stack that small the margin never does, so the deferred objects were leaked when the thread exited, which -R reports as a reference leak on platforms with larger frames such as PPC64LE. Clear the namespace in a cleanup, which runs on the main thread. --- Lib/test/test_lazy_import/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 31006f804fd6d51..1c66d8122893f48 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -318,6 +318,9 @@ def test_deeply_dotted_name(self): ns = {"__builtins__": {"__lazy_import__": __lazy_import__, "__import__": lambda *args: m}} exec("lazy import " + ".".join(["m"] * 10_000) + " as x", ns) + # Free the chain in the main thread: in a thread with a small C stack, + # deallocations deferred by the trashcan are never run. + self.addCleanup(ns.clear) with contextlib.suppress(RecursionError): ns["x"].resolve() From 8825790dc530b3b008445c9151cc7ac9b85d67ac Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 16 Oct 2026 13:34:31 -0700 Subject: [PATCH 2/4] Register the namespace cleanup right after creating it --- Lib/test/test_lazy_import/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index 1c66d8122893f48..d391777f35cf8b9 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -317,10 +317,10 @@ def test_deeply_dotted_name(self): m.m = m ns = {"__builtins__": {"__lazy_import__": __lazy_import__, "__import__": lambda *args: m}} - exec("lazy import " + ".".join(["m"] * 10_000) + " as x", ns) # Free the chain in the main thread: in a thread with a small C stack, # deallocations deferred by the trashcan are never run. self.addCleanup(ns.clear) + exec("lazy import " + ".".join(["m"] * 10_000) + " as x", ns) with contextlib.suppress(RecursionError): ns["x"].resolve() From 30fca0bcf736b96e13e0daf56b959b67e4eceae8 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 16 Oct 2026 13:38:37 -0700 Subject: [PATCH 3/4] Shorten the cleanup comment --- Lib/test/test_lazy_import/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index d391777f35cf8b9..df3b8ae8796860b 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -317,8 +317,7 @@ def test_deeply_dotted_name(self): m.m = m ns = {"__builtins__": {"__lazy_import__": __lazy_import__, "__import__": lambda *args: m}} - # Free the chain in the main thread: in a thread with a small C stack, - # deallocations deferred by the trashcan are never run. + # Free the chain on the main thread, which has the stack to do it. self.addCleanup(ns.clear) exec("lazy import " + ".".join(["m"] * 10_000) + " as x", ns) with contextlib.suppress(RecursionError): From 14471f17c280bae01e7803a3f72337dcc0e75b26 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Fri, 16 Oct 2026 13:39:49 -0700 Subject: [PATCH 4/4] Reword the cleanup comment --- Lib/test/test_lazy_import/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_lazy_import/__init__.py b/Lib/test/test_lazy_import/__init__.py index df3b8ae8796860b..fd74ce6827fcca1 100644 --- a/Lib/test/test_lazy_import/__init__.py +++ b/Lib/test/test_lazy_import/__init__.py @@ -317,7 +317,8 @@ def test_deeply_dotted_name(self): m.m = m ns = {"__builtins__": {"__lazy_import__": __lazy_import__, "__import__": lambda *args: m}} - # Free the chain on the main thread, which has the stack to do it. + # Free the chain on the main thread: a thread with a small C stack + # postpones deep deallocations and never gets back to them. self.addCleanup(ns.clear) exec("lazy import " + ".".join(["m"] * 10_000) + " as x", ns) with contextlib.suppress(RecursionError):