Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 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
575e3f0
Initial version of !p with f-strings.
ericvsmith Feb 22, 2026
cb7e204
Update Objects/object.c
warsaw Feb 23, 2026
e1c45f2
Update Objects/object.c
warsaw Feb 23, 2026
bbf612c
Update Objects/object.c
warsaw Feb 23, 2026
5355fe4
Merge pull request #3 from ericvsmith/pprint
warsaw Feb 23, 2026
90f5b79
ruff can't handle the new !p specifier, so ignore that whole file for…
warsaw Feb 23, 2026
ae8e6e8
Merge branch 'main' into pprint-merge
warsaw Feb 23, 2026
0d8320b
Add a blurb for PEP 813 implementation
warsaw Feb 23, 2026
b331439
False-y names are treated as positional arguments.
warsaw Feb 24, 2026
04a776b
Merge branch 'main' into pprint
warsaw Feb 24, 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
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ repos:
name: Run Ruff (lint) on Lib/test/
args: [--exit-non-zero-on-fix]
files: ^Lib/test/
# TODO: remove this exclude once !p f-string support is merged to main
exclude: ^Lib/test/test_print\.py$
- id: ruff-check
name: Run Ruff (lint) on Tools/build/
args: [--exit-non-zero-on-fix, --config=Tools/build/.ruff.toml]
Expand Down
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 @@ -25,6 +25,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 @@ -250,6 +253,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 @@ -415,3 +446,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)``.
7 changes: 4 additions & 3 deletions Include/ceval.h
Original file line number Diff line number Diff line change
Expand Up @@ -125,13 +125,14 @@ PyAPI_FUNC(void) PyEval_ReleaseThread(PyThreadState *tstate);
}

/* Masks and values used by FORMAT_VALUE opcode. */
#define FVC_MASK 0x3
#define FVC_MASK 0x7
#define FVC_NONE 0x0
#define FVC_STR 0x1
#define FVC_REPR 0x2
#define FVC_ASCII 0x3
#define FVS_MASK 0x4
#define FVS_HAVE_SPEC 0x4
#define FVC_PRETTY 0x4
#define FVS_MASK 0x8
#define FVS_HAVE_SPEC 0x8

#ifndef Py_LIMITED_API
# define Py_CPYTHON_CEVAL_H
Expand Down
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.

1 change: 1 addition & 0 deletions Include/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ PyAPI_FUNC(void) PyType_Modified(PyTypeObject *);
PyAPI_FUNC(PyObject *) PyObject_Repr(PyObject *);
PyAPI_FUNC(PyObject *) PyObject_Str(PyObject *);
PyAPI_FUNC(PyObject *) PyObject_ASCII(PyObject *);
PyAPI_FUNC(PyObject *) PyObject_Pretty(PyObject *);
PyAPI_FUNC(PyObject *) PyObject_Bytes(PyObject *);
PyAPI_FUNC(PyObject *) PyObject_RichCompare(PyObject *, PyObject *, int);
PyAPI_FUNC(int) PyObject_RichCompareBool(PyObject *, PyObject *, int);
Expand Down
48 changes: 48 additions & 0 deletions Lib/pprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,12 +623,60 @@ 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 (str() as name, value) if name:
# 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 (name, value) if not name:
# 2-tuple with a false-y name: treat as positional.
formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1)
parts.append(formatted)
readable = readable and is_readable
case (name, value):
# Truthy non-string name is an error.
raise ValueError(
f"__pprint__ yielded a 2-tuple with "
f"non-string name: {name!r}"
)
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
4 changes: 2 additions & 2 deletions Lib/test/test_fstring.py
Original file line number Diff line number Diff line change
Expand Up @@ -1369,7 +1369,7 @@ def test_conversions(self):
for conv_identifier in 'g', 'A', 'G', 'ä', 'ɐ':
self.assertAllRaise(SyntaxError,
"f-string: invalid conversion character %r: "
"expected 's', 'r', or 'a'" % conv_identifier,
"expected 's', 'r', 'a', or 'p'" % conv_identifier,
["f'{3!" + conv_identifier + "}'"])

for conv_non_identifier in '3', '!':
Expand All @@ -1385,7 +1385,7 @@ def test_conversions(self):

self.assertAllRaise(SyntaxError,
"f-string: invalid conversion character 'ss': "
"expected 's', 'r', or 'a'",
"expected 's', 'r', 'a', or 'p'",
["f'{3!ss}'",
"f'{3!ss:}'",
"f'{3!ss:s}'",
Expand Down
Loading
Loading