https://github.com/python/cpython/commit/8759666e4c00d7b7bcf47bf44dd470b04ab5f708
commit: 8759666e4c00d7b7bcf47bf44dd470b04ab5f708
branch: main
author: Eundo Lee <[email protected]>
committer: hugovk <[email protected]>
date: 2026-09-17T17:55:32+03:00
summary:

gh-155596: Fix pprint expand mode ignoring width for nested values (#155926)

files:
A Misc/NEWS.d/next/Library/2026-08-17-12-07-38.gh-issue-155596.Rt4mZq.rst
M Lib/pprint.py
M Lib/test/test_pprint.py

diff --git a/Lib/pprint.py b/Lib/pprint.py
index 7355021998081d..6b574218277a57 100644
--- a/Lib/pprint.py
+++ b/Lib/pprint.py
@@ -187,6 +187,11 @@ def isreadable(self, object):
         return readable and not recursive
 
     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))
@@ -194,7 +199,7 @@ 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
+        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
@@ -232,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:
@@ -303,13 +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,
             self._child_indent(indent, len(cls.__name__) + 1),
             allowance + 1,
             context,
             level,
+            len(cls.__name__) + 1,
         )
         stream.write(')')
 
@@ -498,13 +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,
             self._child_indent(indent, 13),
             allowance + 1,
             context,
             level,
+            13,
         )
         stream.write(')')
 
@@ -540,13 +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,
                 self._child_indent(indent, len(rep) + 2),
                 allowance if last else 1,
                 context,
                 level,
+                len(rep) + 2,
             )
             if not last:
                 write(delimnl)
@@ -566,13 +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,
                     self._child_indent(indent, len(key) + 1),
                     allowance if last else 1,
                     context,
                     level,
+                    len(key) + 1,
                 )
             if not last:
                 write(delimnl)
diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py
index 041c2072b9e253..dcb1cd693074cb 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 00000000000000..cbd8bc83868513
--- /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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to