From d03a7d34eddd784e37e7107331ce6b7088d9be2e Mon Sep 17 00:00:00 2001 From: Eundo Lee Date: Mon, 17 Aug 2026 12:22:10 +0900 Subject: [PATCH 1/2] gh-155596: Fix pprint expand mode ignoring width for nested values --- Lib/pprint.py | 25 +++++++++++---- Lib/test/test_pprint.py | 31 +++++++++++++++++-- ...-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst | 5 +++ 3 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst diff --git a/Lib/pprint.py b/Lib/pprint.py index 7355021998081dc..6253af37efc6717 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -186,7 +186,8 @@ def isreadable(self, object): s, readable, recursive = self.format(object, {}, 0, 0) return readable and not recursive - def _format(self, object, stream, indent, allowance, context, level): + def _format(self, object, stream, indent, allowance, context, level, + prefix_len=0): objid = id(object) if objid in context: stream.write(_recursion(object)) @@ -194,7 +195,15 @@ def _format(self, object, stream, indent, allowance, context, level): self._readable = False return rep = self._repr(object, context, level) - max_width = self._width - indent - allowance + # prefix_len is the width of any "key: " or "name=" already written on + # this line. In aligned mode continuation lines start after it, so it + # is folded into the indent. In expand mode children are indented at + # the block level instead, but the prefix still consumes width here. + if self._expand: + max_width = self._width - indent - prefix_len - allowance + else: + indent += prefix_len + max_width = self._width - indent - allowance if len(rep) > max_width: p = self._dispatch.get(type(object).__repr__, None) # Lazy import to improve module import time @@ -306,10 +315,11 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level self._format( list(object.items()), stream, - self._child_indent(indent, len(cls.__name__) + 1), + indent, allowance + 1, context, level, + prefix_len=len(cls.__name__) + 1, ) stream.write(')') @@ -501,10 +511,11 @@ def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level self._format( object.copy(), stream, - self._child_indent(indent, 13), + indent, allowance + 1, context, level, + prefix_len=13, ) stream.write(')') @@ -543,10 +554,11 @@ def _format_dict_items(self, items, stream, indent, allowance, context, self._format( ent, stream, - self._child_indent(indent, len(rep) + 2), + indent, allowance if last else 1, context, level, + prefix_len=len(rep) + 2, ) if not last: write(delimnl) @@ -569,10 +581,11 @@ def _format_namespace_items(self, items, stream, indent, allowance, context, lev self._format( ent, stream, - self._child_indent(indent, len(key) + 1), + indent, allowance if last else 1, context, level, + prefix_len=len(key) + 1, ) if not last: write(delimnl) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 041c2072b9e253a..dcb1cd693074cbb 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1646,6 +1646,26 @@ def test_expand_dict(self): 'corge': 7, }""") + def test_expand_respects_width_with_long_keys(self): + # gh-155596: in expand mode the width of the "key: " prefix was not + # counted when deciding whether a value fits on the current line, so + # values under long keys could overflow width. + obj = {'a' * 12: 1, 'b' * 20: 2, 'c' * 30: {'d' * 5: 3, 'e' * 40: 3}} + result = pprint.pformat(obj, expand=True) + self.assertEqual(result, +"""\ +{ + 'aaaaaaaaaaaa': 1, + 'bbbbbbbbbbbbbbbbbbbb': 2, + 'cccccccccccccccccccccccccccccc': { + 'ddddd': 3, + 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee': 3, + }, +}""") + # The nested value must be broken up rather than overflowing. + self.assertTrue(all(len(line) <= 80 for line in result.splitlines()), + max(result.splitlines(), key=len)) + def test_expand_ordered_dict(self): dummy_ordered_dict = collections.OrderedDict( [ @@ -1895,7 +1915,11 @@ def test_expand_chainmap(self): 'baz': 123, 'corge': 7, 'foo': 'bar', - 'quux': ['foo', 'bar', 'baz'], + 'quux': [ + 'foo', + 'bar', + 'baz', + ], 'qux': { 'baz': 123, 'foo': 'bar', @@ -1939,7 +1963,10 @@ def test_expand_deque(self): 'corge': 7, 'foo': 'bar', 'quux': ['foo', 'bar', 'baz'], - 'qux': {'baz': 123, 'foo': 'bar'}, + 'qux': { + 'baz': 123, + 'foo': 'bar', + }, }, 'foo', 'bar', diff --git a/Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst b/Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst new file mode 100644 index 000000000000000..cbd8bc838685139 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst @@ -0,0 +1,5 @@ +Fix :func:`pprint.pprint` and :func:`pprint.pformat` with ``expand=True`` +not honouring *width* for nested values. The width of the ``'key':`` +prefix was not counted when deciding whether a value fitted on the current +line, so values under long keys could overflow *width* instead of being +expanded. From eb085f928b112c66c06513c4205a8f6f997bb8c8 Mon Sep 17 00:00:00 2001 From: Eundo Lee Date: Wed, 2 Sep 2026 10:42:37 +0900 Subject: [PATCH 2/2] Keep _format()'s signature unchanged for subclasses and hand the prefix width over on the instance instead --- Lib/pprint.py | 58 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 6253af37efc6717..6b574218277a573 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -186,8 +186,12 @@ def isreadable(self, object): s, readable, recursive = self.format(object, {}, 0, 0) return readable and not recursive - def _format(self, object, stream, indent, allowance, context, level, - prefix_len=0): + def _format(self, object, stream, indent, allowance, context, level): + # Width of any "key: " prefix already written on the current line by + # _format_child(). In expand mode `indent` is the block indent and so + # does not include it, but it still consumes width here. + prefix_len = self._pending_prefix_len + self._pending_prefix_len = 0 objid = id(object) if objid in context: stream.write(_recursion(object)) @@ -195,15 +199,7 @@ def _format(self, object, stream, indent, allowance, context, level, self._readable = False return rep = self._repr(object, context, level) - # prefix_len is the width of any "key: " or "name=" already written on - # this line. In aligned mode continuation lines start after it, so it - # is folded into the indent. In expand mode children are indented at - # the block level instead, but the prefix still consumes width here. - if self._expand: - max_width = self._width - indent - prefix_len - allowance - else: - indent += prefix_len - max_width = self._width - indent - allowance + max_width = self._width - indent - prefix_len - allowance if len(rep) > max_width: p = self._dispatch.get(type(object).__repr__, None) # Lazy import to improve module import time @@ -241,6 +237,22 @@ def _child_indent(self, indent, prefix_len): return indent return indent + prefix_len + # Set by _format_child() immediately before it calls _format(), and + # consumed there. Passing it out of band keeps _format()'s signature + # unchanged for third-party subclasses that override it. + _pending_prefix_len = 0 + + def _format_child(self, object, stream, indent, allowance, context, level, + prefix_len): + if self._expand: + # Aligned mode folds the prefix into the indent (see + # _child_indent), so only expand mode needs to report it. + self._pending_prefix_len = prefix_len + try: + self._format(object, stream, indent, allowance, context, level) + finally: + self._pending_prefix_len = 0 + def _write_indent_padding(self, write): if self._expand: if self._indent_per_level > 0: @@ -312,14 +324,14 @@ def _pprint_ordered_dict(self, object, stream, indent, allowance, context, level return cls = object.__class__ stream.write(cls.__name__ + '(') - self._format( + self._format_child( list(object.items()), stream, - indent, + self._child_indent(indent, len(cls.__name__) + 1), allowance + 1, context, level, - prefix_len=len(cls.__name__) + 1, + len(cls.__name__) + 1, ) stream.write(')') @@ -508,14 +520,14 @@ def _pprint_bytearray(self, object, stream, indent, allowance, context, level): def _pprint_mappingproxy(self, object, stream, indent, allowance, context, level): stream.write('mappingproxy(') - self._format( + self._format_child( object.copy(), stream, - indent, + self._child_indent(indent, 13), allowance + 1, context, level, - prefix_len=13, + 13, ) stream.write(')') @@ -551,14 +563,14 @@ def _format_dict_items(self, items, stream, indent, allowance, context, rep = self._repr(key, context, level) write(rep) write(': ') - self._format( + self._format_child( ent, stream, - indent, + self._child_indent(indent, len(rep) + 2), allowance if last else 1, context, level, - prefix_len=len(rep) + 2, + len(rep) + 2, ) if not last: write(delimnl) @@ -578,14 +590,14 @@ def _format_namespace_items(self, items, stream, indent, allowance, context, lev # recursive dataclass repr. write("...") else: - self._format( + self._format_child( ent, stream, - indent, + self._child_indent(indent, len(key) + 1), allowance if last else 1, context, level, - prefix_len=len(key) + 1, + len(key) + 1, ) if not last: write(delimnl)