From 5d5b40d8d3f556bbce4d694a81b4fbf37a3ca225 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 17 Feb 2026 12:54:28 +0100 Subject: [PATCH 1/6] gh-141510: Support frozendict in pprint --- Lib/pprint.py | 12 ++++++++++-- Lib/test/test_pprint.py | 10 ++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 92a2c543ac279c..ffba173c2bb24a 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -220,7 +220,14 @@ def _pprint_dataclass(self, object, stream, indent, allowance, context, level): def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write - write('{') + typ = object.__class__ + if typ is frozendict: + stream.write(typ.__name__ + '({') + end = '})' + indent += len(typ.__name__) + 1 + else: + write('{') + end = '}' if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) @@ -231,9 +238,10 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): items = object.items() self._format_dict_items(items, stream, indent, allowance + 1, context, level) - write('}') + write(end) _dispatch[dict.__repr__] = _pprint_dict + _dispatch[frozendict.__repr__] = _pprint_dict def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): if not len(object): diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 41c337ade7eca1..1061ce155840df 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -330,6 +330,16 @@ def test_basic_line_wrap(self): for type in [dict, dict2]: self.assertEqual(pprint.pformat(type(o)), exp) + exp = """\ +frozendict({'RPM_cal': 0, + 'RPM_cal2': 48059, + 'Speed_cal': 0, + 'controldesk_runtime_us': 0, + 'main_code_runtime_us': 0, + 'read_io_runtime_us': 0, + 'write_io_runtime_us': 43690})""" + self.assertEqual(pprint.pformat(frozendict(o)), exp) + o = range(100) exp = 'dict_keys([%s])' % ',\n '.join(map(str, o)) keys = dict.fromkeys(o).keys() From 6d36afcd7e2dbcebcee52481c916a8afb9aec228 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 12:26:21 +0100 Subject: [PATCH 2/6] Move frozendict to its own function Co-authored-by: devdanzin <74280297+devdanzin@users.noreply.github.com> --- Lib/pprint.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index ffba173c2bb24a..b50f335f52b48d 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -221,13 +221,8 @@ def _pprint_dataclass(self, object, stream, indent, allowance, context, level): def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write typ = object.__class__ - if typ is frozendict: - stream.write(typ.__name__ + '({') - end = '})' - indent += len(typ.__name__) + 1 - else: - write('{') - end = '}' + write('{') + end = '}' if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) @@ -240,8 +235,18 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): context, level) write(end) + def _pprint_frozendict(self, object, stream, indent, allowance, context, level): + write = stream.write + cls = object.__class__ + stream.write(cls.__name__ + '(') + self._pprint_dict(object, stream, + indent + len(cls.__name__) + 1, + allowance + 1, + context, level) + write(')') + _dispatch[dict.__repr__] = _pprint_dict - _dispatch[frozendict.__repr__] = _pprint_dict + _dispatch[frozendict.__repr__] = _pprint_frozendict def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): if not len(object): From 09c2d9baa94aa9e66e1a6712a24edb5efecba462 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 12:29:05 +0100 Subject: [PATCH 3/6] Add tests on keys(), values(), items() --- Lib/test/test_pprint.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 1061ce155840df..73f0120ce5ee88 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -344,16 +344,22 @@ def test_basic_line_wrap(self): exp = 'dict_keys([%s])' % ',\n '.join(map(str, o)) keys = dict.fromkeys(o).keys() self.assertEqual(pprint.pformat(keys), exp) + keys = frozendict.fromkeys(o).keys() + self.assertEqual(pprint.pformat(keys), exp) o = range(100) exp = 'dict_values([%s])' % ',\n '.join(map(str, o)) values = {v: v for v in o}.values() self.assertEqual(pprint.pformat(values), exp) + values = frozendict({v: v for v in o}).values() + self.assertEqual(pprint.pformat(values), exp) o = range(100) exp = 'dict_items([%s])' % ',\n '.join("(%s, %s)" % (i, i) for i in o) items = {v: v for v in o}.items() self.assertEqual(pprint.pformat(items), exp) + items = frozendict({v: v for v in o}).items() + self.assertEqual(pprint.pformat(items), exp) o = range(100) exp = 'odict_keys([%s])' % ',\n '.join(map(str, o)) From fb61467203105da1396957b4a4ee235c991a6be1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 12:30:11 +0100 Subject: [PATCH 4/6] Revert _pprint_dict() changes --- Lib/pprint.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index b50f335f52b48d..cd43268fe78c3b 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -220,9 +220,7 @@ def _pprint_dataclass(self, object, stream, indent, allowance, context, level): def _pprint_dict(self, object, stream, indent, allowance, context, level): write = stream.write - typ = object.__class__ write('{') - end = '}' if self._indent_per_level > 1: write((self._indent_per_level - 1) * ' ') length = len(object) @@ -233,7 +231,9 @@ def _pprint_dict(self, object, stream, indent, allowance, context, level): items = object.items() self._format_dict_items(items, stream, indent, allowance + 1, context, level) - write(end) + write('}') + + _dispatch[dict.__repr__] = _pprint_dict def _pprint_frozendict(self, object, stream, indent, allowance, context, level): write = stream.write @@ -245,7 +245,6 @@ def _pprint_frozendict(self, object, stream, indent, allowance, context, level): context, level) write(')') - _dispatch[dict.__repr__] = _pprint_dict _dispatch[frozendict.__repr__] = _pprint_frozendict def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level): From 3ea8049e614bd7f64fa495f22371418249a3d0b1 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 12:32:16 +0100 Subject: [PATCH 5/6] Add tests on empty frozendict and subclasses --- Lib/pprint.py | 10 ++++++---- Lib/test/test_pprint.py | 24 ++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index cd43268fe78c3b..604d7c20782337 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -239,10 +239,12 @@ def _pprint_frozendict(self, object, stream, indent, allowance, context, level): write = stream.write cls = object.__class__ stream.write(cls.__name__ + '(') - self._pprint_dict(object, stream, - indent + len(cls.__name__) + 1, - allowance + 1, - context, level) + length = len(object) + if length: + self._pprint_dict(object, stream, + indent + len(cls.__name__) + 1, + allowance + 1, + context, level) write(')') _dispatch[frozendict.__repr__] = _pprint_frozendict diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 73f0120ce5ee88..f3860a5d511989 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -67,6 +67,13 @@ class dict3(dict): def __repr__(self): return dict.__repr__(self) +class frozendict2(frozendict): + pass + +class frozendict3(frozendict): + def __repr__(self): + return frozendict.__repr__(self) + class dict_custom_repr(dict): def __repr__(self): return '*'*len(dict.__repr__(self)) @@ -254,18 +261,22 @@ def test_same_as_repr(self): set(), set2(), set3(), frozenset(), frozenset2(), frozenset3(), {}, dict2(), dict3(), + frozendict(), frozendict2(), frozendict3(), {}.keys(), {}.values(), {}.items(), MappingView({}), KeysView({}), ItemsView({}), ValuesView({}), self.assertTrue, pprint, -6, -6, -6-6j, -1.5, "x", b"x", bytearray(b"x"), (3,), [3], {3: 6}, - (1,2), [3,4], {5: 6}, + (1,2), [3,4], tuple2((1,2)), tuple3((1,2)), tuple3(range(100)), [3,4], list2([3,4]), list3([3,4]), list3(range(100)), set({7}), set2({7}), set3({7}), frozenset({8}), frozenset2({8}), frozenset3({8}), - dict2({5: 6}), dict3({5: 6}), + {5: 6}, dict2({5: 6}), dict3({5: 6}), + frozendict({5: 6}), frozendict2({5: 6}), frozendict3({5: 6}), {5: 6}.keys(), {5: 6}.values(), {5: 6}.items(), + frozendict({5: 6}).keys(), frozendict({5: 6}).values(), + frozendict({5: 6}).items(), MappingView({5: 6}), KeysView({5: 6}), ItemsView({5: 6}), ValuesView({5: 6}), range(10, -11, -1), @@ -339,6 +350,15 @@ def test_basic_line_wrap(self): 'read_io_runtime_us': 0, 'write_io_runtime_us': 43690})""" self.assertEqual(pprint.pformat(frozendict(o)), exp) + exp = """\ +frozendict2({'RPM_cal': 0, + 'RPM_cal2': 48059, + 'Speed_cal': 0, + 'controldesk_runtime_us': 0, + 'main_code_runtime_us': 0, + 'read_io_runtime_us': 0, + 'write_io_runtime_us': 43690})""" + self.assertEqual(pprint.pformat(frozendict2(o)), exp) o = range(100) exp = 'dict_keys([%s])' % ',\n '.join(map(str, o)) From 9afd4bb6a892942d862d46a32a5a5a623346c349 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Sat, 21 Feb 2026 13:01:03 +0100 Subject: [PATCH 6/6] Add frozendict support to _safe_repr() --- Lib/pprint.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 604d7c20782337..e111bd59d4152c 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -637,12 +637,21 @@ def _safe_repr(self, object, context, maxlevels, level): else: return repr(object), True, False - if issubclass(typ, dict) and r is dict.__repr__: + if ((issubclass(typ, dict) and r is dict.__repr__) + or (issubclass(typ, frozendict) and r is frozendict.__repr__)): + is_frozendict = issubclass(typ, frozendict) if not object: - return "{}", True, False + if is_frozendict: + rep = f"{object.__class__.__name__}()" + else: + rep = "{}" + return rep, True, False objid = id(object) if maxlevels and level >= maxlevels: - return "{...}", False, objid in context + rep = "{...}" + if is_frozendict: + rep = f"{object.__class__.__name__}({rep})" + return rep, False, objid in context if objid in context: return _recursion(object), False, True context[objid] = 1 @@ -665,7 +674,10 @@ def _safe_repr(self, object, context, maxlevels, level): if krecur or vrecur: recursive = True del context[objid] - return "{%s}" % ", ".join(components), readable, recursive + rep = "{%s}" % ", ".join(components) + if is_frozendict: + rep = f"{object.__class__.__name__}({rep})" + return rep, readable, recursive if (issubclass(typ, list) and r is list.__repr__) or \ (issubclass(typ, tuple) and r is tuple.__repr__):