Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
cbb0b0a
Plumb optional `pretty` argument into the `print()` function.
warsaw Oct 25, 2025
7de0338
Kinda works, at least for passing in an explicit `pretty` object.
warsaw Oct 25, 2025
722a72b
Fix pretty=True and remove debugging
warsaw Oct 25, 2025
e84ef57
Call object's __pprint__() function if it has one
warsaw Oct 26, 2025
e800b63
Add some print(..., pretty=) tests
warsaw Oct 28, 2025
9ed491a
Flesh out the pprint protocol documentation
warsaw Nov 3, 2025
fe61924
The pre-PEP
warsaw Nov 4, 2025
8398cc5
Fix a doc lint warning
warsaw Nov 4, 2025
955459e
Add some examples
warsaw Nov 4, 2025
5b6ce45
__pretty__() is now exactly signatured as PrettyPrinter.format()
warsaw Nov 5, 2025
c304a29
Title
warsaw Nov 5, 2025
ab0f4ec
Plumb optional `pretty` argument into the `print()` function.
warsaw Oct 25, 2025
313ccd1
Kinda works, at least for passing in an explicit `pretty` object.
warsaw Oct 25, 2025
d6766c6
Fix pretty=True and remove debugging
warsaw Oct 25, 2025
71dcfe7
Call object's __pprint__() function if it has one
warsaw Oct 26, 2025
4d237da
Add some print(..., pretty=) tests
warsaw Oct 28, 2025
bb23638
Flesh out the pprint protocol documentation
warsaw Nov 3, 2025
3965667
The pre-PEP
warsaw Nov 4, 2025
3e20e37
Fix a doc lint warning
warsaw Nov 4, 2025
7786ec1
Add some examples
warsaw Nov 4, 2025
5060701
__pretty__() is now exactly signatured as PrettyPrinter.format()
warsaw Nov 5, 2025
41cd667
Title
warsaw Nov 5, 2025
52a6a46
Merge branch 'pprint' of github.com:warsaw/cpython into pprint
warsaw Nov 5, 2025
5c6872f
PEP submitted: https://github.com/python/peps/pull/4690
warsaw Nov 7, 2025
28fa67d
Remove an obsolete comment
warsaw Nov 7, 2025
055eda7
Use a rich.pretty compatible pretty printer API
warsaw Dec 14, 2025
8bbb19f
Merge branch 'main' into pprint
warsaw Dec 22, 2025
840e961
Update the pretty print protocol documentation
warsaw Dec 22, 2025
3073891
Use a match statement in _format_pprint()
warsaw Dec 22, 2025
21734be
Improve the pretty print test protocol
warsaw Dec 22, 2025
8944f30
Merge branch 'main' into pprint-merge
warsaw Feb 17, 2026
3aaa402
Fix doc lint error
warsaw Feb 17, 2026
7fc13b2
Fix a cross-reference
warsaw Feb 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions Doc/library/functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1598,17 +1598,23 @@ are always available. They are listed here in alphabetical order.
supported.


.. function:: print(*objects, sep=' ', end='\n', file=None, flush=False)
.. function:: print(*objects, sep=' ', end='\n', file=None, flush=False, pretty=None)

Print *objects* to the text stream *file*, separated by *sep* and followed
by *end*. *sep*, *end*, *file*, and *flush*, if present, must be given as keyword
arguments.
Print *objects* to the text stream *file*, separated by *sep* and followed by
*end*. *sep*, *end*, *file*, *flush*, and *pretty*, if present, must be
given as keyword arguments.

When *pretty* is ``None``, all non-keyword arguments are converted to
strings like :func:`str` does and written to the stream, separated by *sep*
and followed by *end*. Both *sep* and *end* must be strings; they can also
be ``None``, which means to use the default values. If no *objects* are
given, :func:`print` will just write *end*.

All non-keyword arguments are converted to strings like :func:`str` does and
written to the stream, separated by *sep* and followed by *end*. Both *sep*
and *end* must be strings; they can also be ``None``, which means to use the
default values. If no *objects* are given, :func:`print` will just write
*end*.
When *pretty* is given, it signals that the objects should be "pretty
printed". *pretty* can be ``True`` or an object implementing the
:meth:`pprint.PrettyPrinter.pformat` API which takes an object and returns a
formatted representation of the object. When *pretty* is ``True``, then it
calls ``PrettyPrinter.pformat()`` explicitly.

The *file* argument must be an object with a ``write(string)`` method; if it
is not present or ``None``, :data:`sys.stdout` will be used. Since printed
Expand All @@ -1622,6 +1628,9 @@ are always available. They are listed here in alphabetical order.
.. versionchanged:: 3.3
Added the *flush* keyword argument.

.. versionchanged:: 3.15
Added the *pretty* keyword argument.


.. class:: property(fget=None, fset=None, fdel=None, doc=None)

Expand Down
66 changes: 66 additions & 0 deletions Doc/library/pprint.rst
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ adjustable by the *width* parameter defaulting to 80 characters.
.. versionchanged:: 3.10
Added support for pretty-printing :class:`dataclasses.dataclass`.

.. versionchanged:: 3.15
Added support for the :ref:`__pprint__ <dunder-pprint>` protocol.

.. _pprint-functions:

Functions
Expand Down Expand Up @@ -253,6 +256,34 @@ are converted to strings. The default implementation uses the internals of the
calls. The fourth argument, *level*, gives the current level; recursive calls
should be passed a value less than that of the current call.

.. _dunder-pprint:

The "__pprint__" protocol
-------------------------

Pretty printing uses an object's ``__repr__`` by default. For custom pretty printing, objects can
implement a ``__pprint__()`` function to customize how their representations will be printed. If this method
exists, it is called instead of ``__repr__``. The method is called with a single argument, the object to be
pretty printed.

The method is expected to return or yield a sequence of values, which are used to construct a pretty
representation of the object. These values are wrapped in standard class "chrome", such as the class name.
The printed representation will usually look like a class constructor, with positional, keyword, and default
arguments. The values can be any of the following formats:

* A single value, representing a positional argument. The value itself is used.
* A 2-tuple of ``(name, value)`` representing a keyword argument. A representation of
``name=value`` is used.
* A 3-tuple of ``(name, value, default_value)`` representing a keyword argument with a default
value. If ``value`` equals ``default_value``, then this tuple is skipped, otherwise
``name=value`` is used.

.. note::

This protocol is compatible with the `Rich library's pretty printing protocol
<https://rich.readthedocs.io/en/latest/pretty.html#rich-repr-protocol>`_.

See the :ref:`pprint-protocol-example` for how this can be used in practice.

.. _pprint-example:

Expand Down Expand Up @@ -418,3 +449,38 @@ cannot be split, the specified width will be exceeded::
'requires_python': None,
'summary': 'A sample Python project',
'version': '1.2.0'}

.. _pprint-protocol-example:

Pretty Print Protocol Example
-----------------------------

Let's start with a simple class that defines a ``__pprint__()`` method:

.. code-block:: python

class Bass:
def __init__(self, strings: int, pickups: str, active: bool=False):
self._strings = strings
self._pickups = pickups
self._active = active

def __pprint__(self):
yield self._strings
yield 'pickups', self._pickups
yield 'active', self._active, False

precision = Bass(4, 'split coil P', active=False)
stingray = Bass(5, 'humbucker', active=True)

The ``__pprint__()`` method yields three values, which correspond to the ``__init__()`` arguments,
showing by example each of the three different allowed formats. Here is what the output looks like:

.. code-block:: pycon

>>> pprint.pprint(precision)
Bass(4, pickups='split coil P')
>>> pprint.pprint(stingray)
Bass(5, pickups='humbucker', active=True)

Note that you'd get exactly the same output if you used ``print(..., pretty=True)``.
1 change: 1 addition & 0 deletions Include/internal/pycore_global_objects_fini_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Include/internal/pycore_global_strings.h
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ struct _Py_global_strings {
STRUCT_FOR_ID(posix)
STRUCT_FOR_ID(prec)
STRUCT_FOR_ID(preserve_exc)
STRUCT_FOR_ID(pretty)
STRUCT_FOR_ID(print_file_and_line)
STRUCT_FOR_ID(priority)
STRUCT_FOR_ID(progress)
Expand Down
1 change: 1 addition & 0 deletions Include/internal/pycore_runtime_init_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions Include/internal/pycore_unicodeobject_generated.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions Lib/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -609,12 +609,49 @@ def _pprint_user_string(self, object, stream, indent, allowance, context, level)

_dispatch[_collections.UserString.__repr__] = _pprint_user_string

def _format_pprint(self, object, method, context, maxlevels, level):
"""Format an object using its __pprint__ method.

The __pprint__ method should be a generator yielding values:
- yield value -> positional arg
- yield (name, value) -> keyword arg, always shown
- yield (name, value, default) -> keyword arg, shown if value != default
"""
cls_name = type(object).__name__
parts = []
readable = True

for item in method(object):
match item:
case (name, value, default):
# Keyword argument w/default. Show only if value != default.
if value != default:
formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1)
parts.append(f"{name}={formatted}")
readable = readable and is_readable
case (name, value):
# Keyword argument. Always show.
formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1)
parts.append(f"{name}={formatted}")
readable = readable and is_readable
case _:
# Positional argument.
formatted, is_readable, _ = self.format(item, context, maxlevels, level + 1)
parts.append(formatted)
readable = readable and is_readable

rep = f"{cls_name}({', '.join(parts)})"
return rep, readable, False

def _safe_repr(self, object, context, maxlevels, level):
# Return triple (repr_string, isreadable, isrecursive).
typ = type(object)
if typ in _builtin_scalars:
return repr(object), True, False

if (p := getattr(typ, "__pprint__", None)):
return self._format_pprint(object, p, context, maxlevels, level)

r = getattr(typ, "__repr__", None)

if issubclass(typ, int) and r is int.__repr__:
Expand Down
94 changes: 94 additions & 0 deletions Lib/test/test_pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ def __ne__(self, other):
def __hash__(self):
return self._hash


class QueryTestCase(unittest.TestCase):

def setUp(self):
Expand Down Expand Up @@ -1472,6 +1473,99 @@ def test_user_string(self):
'jumped over a '
'lazy dog'}""")

def test_custom_pprinter(self):
# Test __pprint__ with positional and keyword argument.
class CustomPrintable:
def __init__(self, name="my pprint", value=42, is_custom=True):
self.name = name
self.value = value

def __pprint__(self):
yield self.name
yield "value", self.value

stream = io.StringIO()
pprint.pprint(CustomPrintable(), stream=stream)
self.assertEqual(stream.getvalue(), "CustomPrintable('my pprint', value=42)\n")

def test_pprint_protocol_positional(self):
# Test __pprint__ with positional arguments only
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __pprint__(self):
yield self.x
yield self.y

stream = io.StringIO()
pprint.pprint(Point(1, 2), stream=stream)
self.assertEqual(stream.getvalue(), "Point(1, 2)\n")

def test_pprint_protocol_keyword(self):
# Test __pprint__ with keyword arguments
class Config:
def __init__(self, host, port):
self.host = host
self.port = port
def __pprint__(self):
yield ("host", self.host)
yield ("port", self.port)

stream = io.StringIO()
pprint.pprint(Config("localhost", 8080), stream=stream)
self.assertEqual(stream.getvalue(), "Config(host='localhost', port=8080)\n")

def test_pprint_protocol_default(self):
# Test __pprint__ with default values (3-tuple form)
class Bass:
def __init__(self, strings: int, pickups: str, active: bool=False):
self._strings = strings
self._pickups = pickups
self._active = active

def __pprint__(self):
yield self._strings
yield 'pickups', self._pickups
yield 'active', self._active, False

# Defaults should be hidden if the value is equal to the default.
stream = io.StringIO()
pprint.pprint(Bass(4, 'split coil P'), stream=stream)
self.assertEqual(stream.getvalue(), "Bass(4, pickups='split coil P')\n")
# Show the argument if the value is not equal to the default.
stream = io.StringIO()
pprint.pprint(Bass(5, 'humbucker', active=True), stream=stream)
self.assertEqual(stream.getvalue(), "Bass(5, pickups='humbucker', active=True)\n")

def test_pprint_protocol_nested(self):
# Test __pprint__ with nested objects.
class Container:
def __init__(self, items):
self.items = items
def __pprint__(self):
yield "items", self.items

stream = io.StringIO()
c = Container([1, 2, 3])
pprint.pprint(c, stream=stream)
self.assertEqual(stream.getvalue(), "Container(items=[1, 2, 3])\n")
# Nested in a list
stream = io.StringIO()
pprint.pprint([c], stream=stream)
self.assertEqual(stream.getvalue(), "[Container(items=[1, 2, 3])]\n")

def test_pprint_protocol_isreadable(self):
# Test that isreadable works correctly with __pprint__
class Readable:
def __pprint__(self):
yield 42
class Unreadable:
def __pprint__(self):
yield open # built-in function, not readable
self.assertTrue(pprint.isreadable(Readable()))
self.assertFalse(pprint.isreadable(Unreadable()))


class DottedPrettyPrinter(pprint.PrettyPrinter):

Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_print.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import unittest
import sys
from io import StringIO
from pprint import PrettyPrinter

from test import support

Expand Down Expand Up @@ -200,5 +201,44 @@ def test_string_in_loop_on_same_line(self):
str(context.exception))


class PPrintable:
def __pprint__(self):
yield 'I feel pretty'


class PrettySmart(PrettyPrinter):
def pformat(self, obj):
if isinstance(obj, str):
return obj
return super().pformat(obj)


class TestPrettyPrinting(unittest.TestCase):
"""Test the optional `pretty` keyword argument."""

def setUp(self):
self.file = StringIO()

def test_default_pretty(self):
print('one', 2, file=self.file, pretty=None)
self.assertEqual(self.file.getvalue(), 'one 2\n')

def test_default_pretty_printer(self):
print('one', 2, file=self.file, pretty=True)
self.assertEqual(self.file.getvalue(), "'one' 2\n")

def test_pprint_magic(self):
print('one', PPrintable(), 2, file=self.file, pretty=True)
self.assertEqual(self.file.getvalue(), "'one' PPrintable('I feel pretty') 2\n")

def test_custom_pprinter(self):
print('one', PPrintable(), 2, file=self.file, pretty=PrettySmart())
self.assertEqual(self.file.getvalue(), "one PPrintable('I feel pretty') 2\n")

def test_bad_pprinter(self):
with self.assertRaises(AttributeError):
print('one', PPrintable(), 2, file=self.file, pretty=object())


if __name__ == "__main__":
unittest.main()
Loading
Loading