From cbcaae032ca74fa8074160de857bd6b06c634c05 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sun, 8 Mar 2026 20:47:05 -0700 Subject: [PATCH 1/5] gh-145492: Fix defaultdict __repr__ infinite recursion Move Py_ReprLeave(dd->default_factory) inside the else branch so it is only called when Py_ReprEnter returned 0 (successfully entered). When Py_ReprEnter detects recursion (returns > 0), it does not add a new entry to the repr tracking list. Calling Py_ReprLeave in that case incorrectly removed the entry from the outer (non-recursive) call, which allowed subsequent recursive calls to bypass the guard entirely, leading to infinite recursion. Includes a regression test. Co-Authored-By: Claude Opus 4.6 --- Lib/test/test_defaultdict.py | 15 +++++++++++++++ Modules/_collectionsmodule.c | 5 +++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_defaultdict.py b/Lib/test/test_defaultdict.py index fbd7354a915a0a..732e9a876ca8ad 100644 --- a/Lib/test/test_defaultdict.py +++ b/Lib/test/test_defaultdict.py @@ -204,5 +204,20 @@ def default_factory(): self.assertEqual(test_dict[key], 2) self.assertEqual(count, 2) + def test_repr_recursive_factory(self): + # gh-145492: defaultdict.__repr__ should not cause infinite recursion + # when the factory's __repr__ calls repr() on the defaultdict. + class ProblematicFactory: + def __call__(self): + return {} + def __repr__(self): + repr(dd) + return "ProblematicFactory()" + + dd = defaultdict(ProblematicFactory()) + # Should not raise RecursionError + r = repr(dd) + self.assertIn('ProblematicFactory()', r) + if __name__ == "__main__": unittest.main() diff --git a/Modules/_collectionsmodule.c b/Modules/_collectionsmodule.c index c3d63c8aab4b47..15c9aa41911822 100644 --- a/Modules/_collectionsmodule.c +++ b/Modules/_collectionsmodule.c @@ -2385,9 +2385,10 @@ defdict_repr(PyObject *op) } defrepr = PyUnicode_FromString("..."); } - else + else { defrepr = PyObject_Repr(dd->default_factory); - Py_ReprLeave(dd->default_factory); + Py_ReprLeave(dd->default_factory); + } } if (defrepr == NULL) { Py_DECREF(baserepr); From 5ab20700c141d13aa37eefb754ceea42ac45be01 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sun, 8 Mar 2026 20:51:15 -0700 Subject: [PATCH 2/5] Add NEWS entry for defaultdict __repr__ recursion fix Co-Authored-By: Claude Opus 4.6 --- .../next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst new file mode 100644 index 00000000000000..66aa2b929889cb --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst @@ -0,0 +1,2 @@ +Fix infinite recursion in :class:`collections.defaultdict` ``__repr__`` +when a ``defaultdict`` contains itself. From eefe96e9e7e6986f198aedb7c959ada6081c7c4e Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Mon, 9 Mar 2026 00:06:41 -0700 Subject: [PATCH 3/5] Credit KowalskiThomas for bug analysis in NEWS entry Co-Authored-By: Claude Opus 4.6 --- .../Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst index 66aa2b929889cb..173d7eeee9fab9 100644 --- a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst +++ b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst @@ -1,2 +1,3 @@ Fix infinite recursion in :class:`collections.defaultdict` ``__repr__`` -when a ``defaultdict`` contains itself. +when a ``defaultdict`` contains itself. Based on analysis by KowalskiThomas +in :issue:`145492`. From c4dfad8296d781654e41dfb8b8f575f069572b70 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Mon, 9 Mar 2026 09:16:20 -0700 Subject: [PATCH 4/5] =?UTF-8?q?=F0=9F=93=B0=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- .../next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst b/Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst new file mode 100644 index 00000000000000..161da4926e5756 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst @@ -0,0 +1,2 @@ +Fix :class:`collections.defaultdict` ``__repr__`` entering infinite recursion +when the default factory references the ``defaultdict`` instance. From 0feb12075114b923ca48aba5ffbbb16952b4df95 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Mon, 9 Mar 2026 09:36:07 -0700 Subject: [PATCH 5/5] fix(news): use :gh: instead of :issue: and remove duplicate blurb Address review feedback from StanFromIreland: - Changed :issue:`145492` to :gh:`145492` (correct syntax for GitHub issues) - Removed duplicate NEWS blurb added by blurb_it Co-Authored-By: Claude Opus 4.6 --- .../next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst | 2 +- .../next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst diff --git a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst index 173d7eeee9fab9..297ee4099f12c5 100644 --- a/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst +++ b/Misc/NEWS.d/next/Library/2026-03-09-00-00-00.gh-issue-145492.457Afc.rst @@ -1,3 +1,3 @@ Fix infinite recursion in :class:`collections.defaultdict` ``__repr__`` when a ``defaultdict`` contains itself. Based on analysis by KowalskiThomas -in :issue:`145492`. +in :gh:`145492`. diff --git a/Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst b/Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst deleted file mode 100644 index 161da4926e5756..00000000000000 --- a/Misc/NEWS.d/next/Library/2026-03-09-16-16-09.gh-issue-145492.9BCFju.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix :class:`collections.defaultdict` ``__repr__`` entering infinite recursion -when the default factory references the ``defaultdict`` instance.