From cbb0b0a65206c169e5996667c42e66fe01dbbddf Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 13:47:24 -0700 Subject: [PATCH 01/30] Plumb optional `pretty` argument into the `print()` function. --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 4 ++ Python/bltinmodule.c | 37 ++++++++++++++++++- Python/clinic/bltinmodule.c.h | 34 +++++++++++------ 6 files changed, 64 insertions(+), 14 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 92ded14891a101..e7b021cd5ca2a1 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1965,6 +1965,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(posix)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(prec)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(preserve_exc)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pretty)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(print_file_and_line)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(priority)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index cd21b0847b7cdd..7224b58e8f1b1f 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -688,6 +688,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) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 50d82d0a365037..d8dc6654edbe00 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1963,6 +1963,7 @@ extern "C" { INIT_ID(posix), \ INIT_ID(prec), \ INIT_ID(preserve_exc), \ + INIT_ID(pretty), \ INIT_ID(print_file_and_line), \ INIT_ID(priority), \ INIT_ID(progress), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index b4d920154b6e83..eb02592d74a7be 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2540,6 +2540,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(pretty); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(print_file_and_line); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index f6fadd936bb8ff..ba86828aeeb7e7 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2213,6 +2213,8 @@ print as builtin_print a file-like object (stream); defaults to the current sys.stdout. flush: bool = False whether to forcibly flush the stream. + pretty: object = None + a pretty-printing object, None, or True. Prints the values to a stream, or to sys.stdout by default. @@ -2221,10 +2223,11 @@ Prints the values to a stream, or to sys.stdout by default. static PyObject * builtin_print_impl(PyObject *module, PyObject * const *objects, Py_ssize_t objects_length, PyObject *sep, PyObject *end, - PyObject *file, int flush) -/*[clinic end generated code: output=38d8def56c837bcc input=ff35cb3d59ee8115]*/ + PyObject *file, int flush, PyObject *pretty) +/*[clinic end generated code: output=2c26c52acf1807b9 input=e5c1e64da822042c]*/ { int i, err; + PyObject *printer = NULL; if (file == Py_None) { file = PySys_GetAttr(&_Py_ID(stdout)); @@ -2262,6 +2265,31 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } + if (pretty == Py_True) { + /* Use default `pprint.PrettyPrinter` */ + PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); + PyObject *printer = NULL; + + if (!printer_factory) { + Py_DECREF(file); + return NULL; + } + printer = PyObject_CallNoArgs(printer_factory); + Py_DECREF(printer_factory); + + if (!printer) { + Py_DECREF(file); + return NULL; + } + } + else if (pretty == Py_None) { + /* Don't use a pretty printer */ + } + else { + /* Use the given object as the pretty printer */ + printer = pretty; + Py_INCREF(printer); + } for (i = 0; i < objects_length; i++) { if (i > 0) { @@ -2273,12 +2301,14 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, } if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } @@ -2291,16 +2321,19 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, } if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } if (flush) { if (_PyFile_Flush(file) < 0) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } Py_DECREF(file); + Py_XDECREF(printer); Py_RETURN_NONE; } diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index adb82f45c25b5d..8934d3b78bc398 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -920,7 +920,8 @@ builtin_pow(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject } PyDoc_STRVAR(builtin_print__doc__, -"print($module, /, *objects, sep=\' \', end=\'\\n\', file=None, flush=False)\n" +"print($module, /, *objects, sep=\' \', end=\'\\n\', file=None, flush=False,\n" +" pretty=None)\n" "--\n" "\n" "Prints the values to a stream, or to sys.stdout by default.\n" @@ -932,7 +933,9 @@ PyDoc_STRVAR(builtin_print__doc__, " file\n" " a file-like object (stream); defaults to the current sys.stdout.\n" " flush\n" -" whether to forcibly flush the stream."); +" whether to forcibly flush the stream.\n" +" pretty\n" +" a pretty-printing object, None, or True."); #define BUILTIN_PRINT_METHODDEF \ {"print", _PyCFunction_CAST(builtin_print), METH_FASTCALL|METH_KEYWORDS, builtin_print__doc__}, @@ -940,7 +943,7 @@ PyDoc_STRVAR(builtin_print__doc__, static PyObject * builtin_print_impl(PyObject *module, PyObject * const *objects, Py_ssize_t objects_length, PyObject *sep, PyObject *end, - PyObject *file, int flush); + PyObject *file, int flush, PyObject *pretty); static PyObject * builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -948,7 +951,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 4 + #define NUM_KEYWORDS 5 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -957,7 +960,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), }, + .ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), &_Py_ID(pretty), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -966,14 +969,14 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"sep", "end", "file", "flush", NULL}; + static const char * const _keywords[] = {"sep", "end", "file", "flush", "pretty", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "print", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[4]; + PyObject *argsbuf[5]; PyObject * const *fastargs; Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; PyObject * const *objects; @@ -982,6 +985,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *end = Py_None; PyObject *file = Py_None; int flush = 0; + PyObject *pretty = Py_None; fastargs = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); @@ -1009,14 +1013,20 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec goto skip_optional_kwonly; } } - flush = PyObject_IsTrue(fastargs[3]); - if (flush < 0) { - goto exit; + if (fastargs[3]) { + flush = PyObject_IsTrue(fastargs[3]); + if (flush < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } } + pretty = fastargs[4]; skip_optional_kwonly: objects = args; objects_length = nargs; - return_value = builtin_print_impl(module, objects, objects_length, sep, end, file, flush); + return_value = builtin_print_impl(module, objects, objects_length, sep, end, file, flush, pretty); exit: return return_value; @@ -1277,4 +1287,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=7eada753dc2e046f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=45f6ee7f6eb4bd75 input=a9049054013a1b77]*/ From 7de0338c0d8785c61483384080b69844b382bc43 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 14:23:02 -0700 Subject: [PATCH 02/30] Kinda works, at least for passing in an explicit `pretty` object. pretty=True doesn't work yet. --- Python/bltinmodule.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index ba86828aeeb7e7..eace5e51aac37f 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2270,6 +2270,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); PyObject *printer = NULL; + PyObject_Print(printer_factory, stderr, 0); if (!printer_factory) { Py_DECREF(file); return NULL; @@ -2281,6 +2282,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } + PyObject_Print(printer, stderr, 0); } else if (pretty == Py_None) { /* Don't use a pretty printer */ @@ -2305,7 +2307,33 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, return NULL; } } - err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); + /* XXX: I have a couple of thoughts about how this could be handled. We could add a + PyFile_WriteObjectEx() function which would look largely like PyFile_WriteObject() but + would take a pretty printer object (or None, in which case it would just fall back to + PyFile_WriteObject()). Then we could put the logic for the (TBD) "pretty printing + protocol" in there. + + For now though, let's keep things localized so all the logic is in the print() function's + implementation. Maybe a better way will come to mind as we pan this idea out. + + Or, this currently calls `printer.pformat(object)` so a pretty printing protocol could + be implemented there. Or maybe we want a more generic method name. + */ + PyObject_Print(printer, stderr, 0); + if (printer) { + PyObject *prettified = PyObject_CallMethod(printer, "pformat", "O", objects[i]); + + if (!prettified) { + Py_DECREF(file); + Py_DECREF(printer); + return NULL; + } + err = PyFile_WriteObject(prettified, file, Py_PRINT_RAW); + Py_XDECREF(prettified); + } + else { + err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); + } if (err) { Py_DECREF(file); Py_XDECREF(printer); From 722a72b3ccedcad52d8017f5730f60e18bb86c14 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 15:23:09 -0700 Subject: [PATCH 03/30] Fix pretty=True and remove debugging --- Python/bltinmodule.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index eace5e51aac37f..55568e58dcdd8a 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2268,9 +2268,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, if (pretty == Py_True) { /* Use default `pprint.PrettyPrinter` */ PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); - PyObject *printer = NULL; - PyObject_Print(printer_factory, stderr, 0); if (!printer_factory) { Py_DECREF(file); return NULL; @@ -2282,7 +2280,6 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } - PyObject_Print(printer, stderr, 0); } else if (pretty == Py_None) { /* Don't use a pretty printer */ @@ -2319,7 +2316,6 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Or, this currently calls `printer.pformat(object)` so a pretty printing protocol could be implemented there. Or maybe we want a more generic method name. */ - PyObject_Print(printer, stderr, 0); if (printer) { PyObject *prettified = PyObject_CallMethod(printer, "pformat", "O", objects[i]); From e84ef575ecac07b30652281bd5aef34f5132f9c1 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 26 Oct 2025 08:41:33 -0700 Subject: [PATCH 04/30] Call object's __pprint__() function if it has one --- Lib/pprint.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/pprint.py b/Lib/pprint.py index 92a2c543ac279c..fabc90907e1d00 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -615,6 +615,9 @@ def _safe_repr(self, object, context, maxlevels, level): if typ in _builtin_scalars: return repr(object), True, False + if (p := getattr(typ, "__pprint__", None)): + return p(object, context, maxlevels, level), True, False + r = getattr(typ, "__repr__", None) if issubclass(typ, int) and r is int.__repr__: From e800b63f56ecf31e4f017c3daae2e9b5ad3dc848 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 27 Oct 2025 20:30:15 -0700 Subject: [PATCH 05/30] Add some print(..., pretty=) tests --- Lib/test/test_print.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index 12256b3b562637..e8898f54b61edd 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -1,6 +1,7 @@ import unittest import sys from io import StringIO +from pprint import PrettyPrinter from test import support @@ -200,5 +201,44 @@ def test_string_in_loop_on_same_line(self): str(context.exception)) +class PPrintable: + def __pprint__(self, context, maxlevels, level): + return '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' 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 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() From 9ed491a8a13b7fcfa6bafff78bc4d1488b1e35e1 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 11:31:45 -0800 Subject: [PATCH 06/30] Flesh out the pprint protocol documentation * Update the docs on the `print()` function to describe the new `pretty` keyword * Document the `__pprint__()` protocol. * Add a test for the `__pprint__()` protocol method. --- Doc/library/functions.rst | 27 ++++++++++++++++++--------- Doc/library/pprint.rst | 22 ++++++++++++++++++++++ Lib/test/test_pprint.py | 19 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 61799e303a1639..b03033219fbe14 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1587,17 +1587,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 + :method:`PrettyPrinter.pprint()` API which takes an object and returns a + formatted representation of the object. When *pretty* is ``True``, then it + actually does call ``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 @@ -1611,6 +1617,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) diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst index f51892450798ae..5a6af7acec0195 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -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__ ` protocol. + .. _pprint-functions: Functions @@ -253,6 +256,16 @@ 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 will use 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 with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format()`. The +``__pprint__()`` method is expected to return a string, which is used as the pretty printed representation of +the object. .. _pprint-example: @@ -418,3 +431,12 @@ cannot be split, the specified width will be exceeded:: 'requires_python': None, 'summary': 'A sample Python project', 'version': '1.2.0'} + +A custom ``__pprint__()`` method can be used to customize the representation of the object:: + + >>> class Custom: + ... def __str__(self): return 'my str' + ... def __repr__(self): return 'my repr' + ... def __pprint__(self, context, maxlevels, level): return 'my pprint' + >>> pprint.pp(Custom()) + my pprint diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 41c337ade7eca1..daa1dbd8c91ab4 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -134,6 +134,18 @@ def __ne__(self, other): def __hash__(self): return self._hash + +class CustomPrintable: + def __str__(self): + return "my str" + + def __repr__(self): + return "my str" + + def __pprint__(self, context, maxlevels, level): + return "my pprint" + + class QueryTestCase(unittest.TestCase): def setUp(self): @@ -1472,6 +1484,13 @@ def test_user_string(self): 'jumped over a ' 'lazy dog'}""") + def test_custom_pprinter(self): + stream = io.StringIO() + pp = pprint.PrettyPrinter(stream=stream) + custom_obj = CustomPrintable() + pp.pprint(custom_obj) + self.assertEqual(stream.getvalue(), "my pprint\n") + class DottedPrettyPrinter(pprint.PrettyPrinter): From fe619241d6315fa16a7f460872dc68df17c9e234 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 16:41:29 -0800 Subject: [PATCH 07/30] The pre-PEP --- pep-9999.rst | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 pep-9999.rst diff --git a/pep-9999.rst b/pep-9999.rst new file mode 100644 index 00000000000000..af0aadf21f488e --- /dev/null +++ b/pep-9999.rst @@ -0,0 +1,154 @@ +PEP: 9999 +Title: The pprint protocol +Author: Barry Warsaw , + Eric V. Smith +Discussions-To: Pending +Status: Draft +Type: Standards Track +Created: 03-Nov-2025 +Python-Version: 3.15 +Post-History: Pending + + +Abstract +======== + +This PEP describes the "pprint protocol", a collection of changes proposed to make pretty printing more +customizable and convenient. + + +Motivation +========== + +"Pretty printing" is a feature which provides a capability to format object representations for better +readability. The core functionality is implemented by the standard library :mod:`pprint`. ``pprint`` +includes a class and APIs which users can invoke to format and print more readable representations of objects. +Important use cases include pretty printing large dictionaries and other complicated objects. + +The ``pprint`` module is great as far as it goes. This PEP builds on the features of this module to provide +more customization and convenience. + + +Rationale +========= + +Pretty printing is very useful for displaying complex data structures, like dictionaries read from JSON +content. By providing a way for classes to customize how their instances participate in pretty printing, +users have more options for visually improving the display and debugging of their complex data. + +By extending the built-in :py:func:`print` function to automatically pretty print its output, this feature is +made even more convenient, since no extra imports are required, and users can easily just piggyback on +well-worn "print debugging" patterns, at least for the most common use cases. + +These two extensions work independently, but hand-in-hand can provide a powerful and convenient new feature. + + +Specification +============= + +There are two parts to this proposal. + + +``__pretty__()`` methods +------------------------ + +Classes can implement a new dunder method, ``__pretty__()`` which if present, generates the pretty printed +representation of their instances. This augments ``__repr__()`` which, prior to this proposal, was the only +method used to generate a pretty representation of the object. Since the built-in :py:func:`repr` function +provides functionality potentially separate from pretty printing, some classes may want more control over +object display between those two use cases. + +``__pretty__()`` is optional; if missing, the standard pretty printers fall back to ``__repr__()`` for full +backward compatibility. However, if defined on a class, ``__pretty__()`` has the same argument signature as +:py:func:`PrettyPrinter.format`, taking four arguments: + +* ``object`` - the object to print, which is effectively always ``self`` +* ``context`` - a dictionary mapping the ``id()`` of objects which are part of the current presentation + context +* ``maxlevels`` - the requested limit to recursion +* ``levels`` - the current recursion level + +See :py:func:`PrettyPrinter.format` for details. + +Unlike that function, ``__pretty__()`` returns a single value, the string to be used as the pretty printed +representation. + + +A new argument to built-in ``print`` +------------------------------------ + +Built-in :py:func:`print` takes a new optional argument, appended to the end of the argument list, called +``pretty``, which can take one of the following values: + +* ``None`` - the default; fully backward compatible +* ``True`` - use a temporary instance of the :py:class:`PrettyPrinter` class to get a pretty representation of + the object. +* An instance with a ``pformat()`` method, which has the same signature as + :meth:`PrettyPrinter.pformat`. When given, this will usually be an instance of a subclass of + `PrettyPrinter` with its `pformat()` method overridden. Note that this form requires **an + instance** of a pretty printer, not a class, as only ``print(..., pretty=True)`` performs implicit + instantiation. + + +Backwards Compatibility +======================= + +When none of the new features are used, this PEP is fully backward compatible, both for built-in +``print()`` and the ``pprint`` module. + + +Security Implications +===================== + +There are no known security implications for this proposal. + + +How to Teach This +================= + +Documentation and examples are added to the ``pprint`` module and the ``print()`` function. +Beginners don't need to be taught these new features until they want prettier representations of +their objects. + + +Reference Implementation +======================== + +The reference implementation is currently available as a `PEP author branch of the CPython main +branch `__. + + +Rejected Ideas +============== + +None at this time. + + +Open Issues +=========== + +As currently defined, the ``__pretty__()`` method is defined as taking four arguments and returning +a single string. This is close to -- but not quite -- the full signature for +``PrettyPrinter.format()``, and was chosen for reference implementation convenience. It's not +clear that custom pretty representations need ``context``, ``maxlevels``, and ``levels``, so perhaps +the argument list should be simplified? If not, then perhaps ``__pretty__()`` should *exactly* +match ``PrettyPrinter.format()``? That does, however complicate the protocol for users. + + +Acknowledgements +================ + +TBD + + +Footnotes +========= + +TBD + + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From 8398cc542f4bee58c46df67b4de102da45a89ca9 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 16:56:15 -0800 Subject: [PATCH 08/30] Fix a doc lint warning --- Doc/library/pprint.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst index 5a6af7acec0195..4ef7a7664477a1 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -263,7 +263,7 @@ The "__pprint__" protocol Pretty printing will use 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 with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format()`. The +exists, it is called with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format`. The ``__pprint__()`` method is expected to return a string, which is used as the pretty printed representation of the object. From 955459e7749bcf5e3dcedb4b3d645a8e5340fc56 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 18:11:46 -0800 Subject: [PATCH 09/30] Add some examples --- pep-9999.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pep-9999.rst b/pep-9999.rst index af0aadf21f488e..dc2b96f0e9e2b8 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -90,6 +90,51 @@ Built-in :py:func:`print` takes a new optional argument, appended to the end of instantiation. +Examples +======== + +A custom ``__pprint__()`` method can be used to customize the representation of the object: + +.. _code-block: + + >>> class Custom: + ... def __str__(self): return 'my str' + ... def __repr__(self): return 'my repr' + ... def __pprint__(self, context, maxlevels, level): return 'my pprint' + + >>> pprint.pp(Custom()) + my pprint + +Using the ``pretty`` argument to ``print()``: + +.. _code-block: + + >>> import os + >>> print(os.pathconf_names) + {'PC_ASYNC_IO': 17, 'PC_CHOWN_RESTRICTED': 7, 'PC_FILESIZEBITS': 18, 'PC_LINK_MAX': 1, 'PC_MAX_CANON': 2, 'PC_MAX_INPUT': 3, 'PC_NAME_MAX': 4, 'PC_NO_TRUNC': 8, 'PC_PATH_MAX': 5, 'PC_PIPE_BUF': 6, 'PC_PRIO_IO': 19, 'PC_SYNC_IO': 25, 'PC_VDISABLE': 9, 'PC_MIN_HOLE_SIZE': 27, 'PC_ALLOC_SIZE_MIN': 16, 'PC_REC_INCR_XFER_SIZE': 20, 'PC_REC_MAX_XFER_SIZE': 21, 'PC_REC_MIN_XFER_SIZE': 22, 'PC_REC_XFER_ALIGN': 23, 'PC_SYMLINK_MAX': 24} + >>> print(os.pathconf_names, pretty=True) + {'PC_ALLOC_SIZE_MIN': 16, + 'PC_ASYNC_IO': 17, + 'PC_CHOWN_RESTRICTED': 7, + 'PC_FILESIZEBITS': 18, + 'PC_LINK_MAX': 1, + 'PC_MAX_CANON': 2, + 'PC_MAX_INPUT': 3, + 'PC_MIN_HOLE_SIZE': 27, + 'PC_NAME_MAX': 4, + 'PC_NO_TRUNC': 8, + 'PC_PATH_MAX': 5, + 'PC_PIPE_BUF': 6, + 'PC_PRIO_IO': 19, + 'PC_REC_INCR_XFER_SIZE': 20, + 'PC_REC_MAX_XFER_SIZE': 21, + 'PC_REC_MIN_XFER_SIZE': 22, + 'PC_REC_XFER_ALIGN': 23, + 'PC_SYMLINK_MAX': 24, + 'PC_SYNC_IO': 25, + 'PC_VDISABLE': 9} + + Backwards Compatibility ======================= From 5b6ce4508c76c20563b7c9618ddffb93b0ea738f Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 5 Nov 2025 13:45:02 -0800 Subject: [PATCH 10/30] __pretty__() is now exactly signatured as PrettyPrinter.format() --- Lib/pprint.py | 2 +- Lib/test/test_pprint.py | 3 ++- Lib/test/test_print.py | 2 +- pep-9999.rst | 26 +++++++++++--------------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index fabc90907e1d00..3b8990dc7f410c 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -616,7 +616,7 @@ def _safe_repr(self, object, context, maxlevels, level): return repr(object), True, False if (p := getattr(typ, "__pprint__", None)): - return p(object, context, maxlevels, level), True, False + return p(object, context, maxlevels, level) r = getattr(typ, "__repr__", None) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index daa1dbd8c91ab4..61d9fa72358f09 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -143,7 +143,8 @@ def __repr__(self): return "my str" def __pprint__(self, context, maxlevels, level): - return "my pprint" + # The custom pretty repr, not-readable bool, no recursion detected. + return "my pprint", False, False class QueryTestCase(unittest.TestCase): diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index e8898f54b61edd..b13380d354e468 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -203,7 +203,7 @@ def test_string_in_loop_on_same_line(self): class PPrintable: def __pprint__(self, context, maxlevels, level): - return 'I feel pretty' + return 'I feel pretty', False, False class PrettySmart(PrettyPrinter): diff --git a/pep-9999.rst b/pep-9999.rst index dc2b96f0e9e2b8..12ca97c2c29d03 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -54,13 +54,13 @@ There are two parts to this proposal. Classes can implement a new dunder method, ``__pretty__()`` which if present, generates the pretty printed representation of their instances. This augments ``__repr__()`` which, prior to this proposal, was the only -method used to generate a pretty representation of the object. Since the built-in :py:func:`repr` function -provides functionality potentially separate from pretty printing, some classes may want more control over -object display between those two use cases. +method used to generate a pretty representation of the object. Since object reprs provide functionality +distinct from pretty printing, some classes may want more control over their pretty display. ``__pretty__()`` is optional; if missing, the standard pretty printers fall back to ``__repr__()`` for full -backward compatibility. However, if defined on a class, ``__pretty__()`` has the same argument signature as -:py:func:`PrettyPrinter.format`, taking four arguments: +backward compatibility (technically speaking, :meth:`pprint.saferepr` is used). However, if defined on a +class, ``__pretty__()`` has the same argument signature as :py:func:`PrettyPrinter.format`, taking four +arguments: * ``object`` - the object to print, which is effectively always ``self`` * ``context`` - a dictionary mapping the ``id()`` of objects which are part of the current presentation @@ -68,10 +68,12 @@ backward compatibility. However, if defined on a class, ``__pretty__()`` has th * ``maxlevels`` - the requested limit to recursion * ``levels`` - the current recursion level -See :py:func:`PrettyPrinter.format` for details. +Similarly, ``__pretty__()`` returns three values, the string to be used as the pretty printed representation, +a boolean indicating whether the returned value is "readable", and a boolean indicating whether recursion has +been detected. In this context, "readable" means the same as :meth:`PrettyPrinter.isreadable`, i.e. that the +returned value can be used to reconstruct the original object using ``eval()``. -Unlike that function, ``__pretty__()`` returns a single value, the string to be used as the pretty printed -representation. +See :py:func:`PrettyPrinter.format` for details. A new argument to built-in ``print`` @@ -172,13 +174,7 @@ None at this time. Open Issues =========== -As currently defined, the ``__pretty__()`` method is defined as taking four arguments and returning -a single string. This is close to -- but not quite -- the full signature for -``PrettyPrinter.format()``, and was chosen for reference implementation convenience. It's not -clear that custom pretty representations need ``context``, ``maxlevels``, and ``levels``, so perhaps -the argument list should be simplified? If not, then perhaps ``__pretty__()`` should *exactly* -match ``PrettyPrinter.format()``? That does, however complicate the protocol for users. - +TBD Acknowledgements ================ From c304a296475f10e1183f3f9c460a0bd1c1093eda Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 5 Nov 2025 14:45:01 -0800 Subject: [PATCH 11/30] Title --- pep-9999.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pep-9999.rst b/pep-9999.rst index 12ca97c2c29d03..4f0b086a63873a 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -1,5 +1,5 @@ PEP: 9999 -Title: The pprint protocol +Title: The Pretty Print Protocol Author: Barry Warsaw , Eric V. Smith Discussions-To: Pending @@ -13,7 +13,7 @@ Post-History: Pending Abstract ======== -This PEP describes the "pprint protocol", a collection of changes proposed to make pretty printing more +This PEP describes the "pretty print protocol", a collection of changes proposed to make pretty printing more customizable and convenient. From ab0f4ece7d1864b34238f21f09c26aaf4854adbb Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 13:47:24 -0700 Subject: [PATCH 12/30] Plumb optional `pretty` argument into the `print()` function. --- .../pycore_global_objects_fini_generated.h | 1 + Include/internal/pycore_global_strings.h | 1 + .../internal/pycore_runtime_init_generated.h | 1 + .../internal/pycore_unicodeobject_generated.h | 4 ++ Python/bltinmodule.c | 37 ++++++++++++++++++- Python/clinic/bltinmodule.c.h | 34 +++++++++++------ 6 files changed, 64 insertions(+), 14 deletions(-) diff --git a/Include/internal/pycore_global_objects_fini_generated.h b/Include/internal/pycore_global_objects_fini_generated.h index 92ded14891a101..e7b021cd5ca2a1 100644 --- a/Include/internal/pycore_global_objects_fini_generated.h +++ b/Include/internal/pycore_global_objects_fini_generated.h @@ -1965,6 +1965,7 @@ _PyStaticObjects_CheckRefcnt(PyInterpreterState *interp) { _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(posix)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(prec)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(preserve_exc)); + _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(pretty)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(print_file_and_line)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(priority)); _PyStaticObject_CheckRefcnt((PyObject *)&_Py_ID(progress)); diff --git a/Include/internal/pycore_global_strings.h b/Include/internal/pycore_global_strings.h index cd21b0847b7cdd..7224b58e8f1b1f 100644 --- a/Include/internal/pycore_global_strings.h +++ b/Include/internal/pycore_global_strings.h @@ -688,6 +688,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) diff --git a/Include/internal/pycore_runtime_init_generated.h b/Include/internal/pycore_runtime_init_generated.h index 50d82d0a365037..d8dc6654edbe00 100644 --- a/Include/internal/pycore_runtime_init_generated.h +++ b/Include/internal/pycore_runtime_init_generated.h @@ -1963,6 +1963,7 @@ extern "C" { INIT_ID(posix), \ INIT_ID(prec), \ INIT_ID(preserve_exc), \ + INIT_ID(pretty), \ INIT_ID(print_file_and_line), \ INIT_ID(priority), \ INIT_ID(progress), \ diff --git a/Include/internal/pycore_unicodeobject_generated.h b/Include/internal/pycore_unicodeobject_generated.h index b4d920154b6e83..eb02592d74a7be 100644 --- a/Include/internal/pycore_unicodeobject_generated.h +++ b/Include/internal/pycore_unicodeobject_generated.h @@ -2540,6 +2540,10 @@ _PyUnicode_InitStaticStrings(PyInterpreterState *interp) { _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); assert(PyUnicode_GET_LENGTH(string) != 1); + string = &_Py_ID(pretty); + _PyUnicode_InternStatic(interp, &string); + assert(_PyUnicode_CheckConsistency(string, 1)); + assert(PyUnicode_GET_LENGTH(string) != 1); string = &_Py_ID(print_file_and_line); _PyUnicode_InternStatic(interp, &string); assert(_PyUnicode_CheckConsistency(string, 1)); diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index f6fadd936bb8ff..ba86828aeeb7e7 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2213,6 +2213,8 @@ print as builtin_print a file-like object (stream); defaults to the current sys.stdout. flush: bool = False whether to forcibly flush the stream. + pretty: object = None + a pretty-printing object, None, or True. Prints the values to a stream, or to sys.stdout by default. @@ -2221,10 +2223,11 @@ Prints the values to a stream, or to sys.stdout by default. static PyObject * builtin_print_impl(PyObject *module, PyObject * const *objects, Py_ssize_t objects_length, PyObject *sep, PyObject *end, - PyObject *file, int flush) -/*[clinic end generated code: output=38d8def56c837bcc input=ff35cb3d59ee8115]*/ + PyObject *file, int flush, PyObject *pretty) +/*[clinic end generated code: output=2c26c52acf1807b9 input=e5c1e64da822042c]*/ { int i, err; + PyObject *printer = NULL; if (file == Py_None) { file = PySys_GetAttr(&_Py_ID(stdout)); @@ -2262,6 +2265,31 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } + if (pretty == Py_True) { + /* Use default `pprint.PrettyPrinter` */ + PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); + PyObject *printer = NULL; + + if (!printer_factory) { + Py_DECREF(file); + return NULL; + } + printer = PyObject_CallNoArgs(printer_factory); + Py_DECREF(printer_factory); + + if (!printer) { + Py_DECREF(file); + return NULL; + } + } + else if (pretty == Py_None) { + /* Don't use a pretty printer */ + } + else { + /* Use the given object as the pretty printer */ + printer = pretty; + Py_INCREF(printer); + } for (i = 0; i < objects_length; i++) { if (i > 0) { @@ -2273,12 +2301,14 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, } if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } @@ -2291,16 +2321,19 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, } if (err) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } if (flush) { if (_PyFile_Flush(file) < 0) { Py_DECREF(file); + Py_XDECREF(printer); return NULL; } } Py_DECREF(file); + Py_XDECREF(printer); Py_RETURN_NONE; } diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h index adb82f45c25b5d..8934d3b78bc398 100644 --- a/Python/clinic/bltinmodule.c.h +++ b/Python/clinic/bltinmodule.c.h @@ -920,7 +920,8 @@ builtin_pow(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject } PyDoc_STRVAR(builtin_print__doc__, -"print($module, /, *objects, sep=\' \', end=\'\\n\', file=None, flush=False)\n" +"print($module, /, *objects, sep=\' \', end=\'\\n\', file=None, flush=False,\n" +" pretty=None)\n" "--\n" "\n" "Prints the values to a stream, or to sys.stdout by default.\n" @@ -932,7 +933,9 @@ PyDoc_STRVAR(builtin_print__doc__, " file\n" " a file-like object (stream); defaults to the current sys.stdout.\n" " flush\n" -" whether to forcibly flush the stream."); +" whether to forcibly flush the stream.\n" +" pretty\n" +" a pretty-printing object, None, or True."); #define BUILTIN_PRINT_METHODDEF \ {"print", _PyCFunction_CAST(builtin_print), METH_FASTCALL|METH_KEYWORDS, builtin_print__doc__}, @@ -940,7 +943,7 @@ PyDoc_STRVAR(builtin_print__doc__, static PyObject * builtin_print_impl(PyObject *module, PyObject * const *objects, Py_ssize_t objects_length, PyObject *sep, PyObject *end, - PyObject *file, int flush); + PyObject *file, int flush, PyObject *pretty); static PyObject * builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) @@ -948,7 +951,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *return_value = NULL; #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - #define NUM_KEYWORDS 4 + #define NUM_KEYWORDS 5 static struct { PyGC_Head _this_is_not_used; PyObject_VAR_HEAD @@ -957,7 +960,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec } _kwtuple = { .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) .ob_hash = -1, - .ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), }, + .ob_item = { &_Py_ID(sep), &_Py_ID(end), &_Py_ID(file), &_Py_ID(flush), &_Py_ID(pretty), }, }; #undef NUM_KEYWORDS #define KWTUPLE (&_kwtuple.ob_base.ob_base) @@ -966,14 +969,14 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec # define KWTUPLE NULL #endif // !Py_BUILD_CORE - static const char * const _keywords[] = {"sep", "end", "file", "flush", NULL}; + static const char * const _keywords[] = {"sep", "end", "file", "flush", "pretty", NULL}; static _PyArg_Parser _parser = { .keywords = _keywords, .fname = "print", .kwtuple = KWTUPLE, }; #undef KWTUPLE - PyObject *argsbuf[4]; + PyObject *argsbuf[5]; PyObject * const *fastargs; Py_ssize_t noptargs = 0 + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 0; PyObject * const *objects; @@ -982,6 +985,7 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec PyObject *end = Py_None; PyObject *file = Py_None; int flush = 0; + PyObject *pretty = Py_None; fastargs = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, /*minpos*/ 0, /*maxpos*/ 0, /*minkw*/ 0, /*varpos*/ 1, argsbuf); @@ -1009,14 +1013,20 @@ builtin_print(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObjec goto skip_optional_kwonly; } } - flush = PyObject_IsTrue(fastargs[3]); - if (flush < 0) { - goto exit; + if (fastargs[3]) { + flush = PyObject_IsTrue(fastargs[3]); + if (flush < 0) { + goto exit; + } + if (!--noptargs) { + goto skip_optional_kwonly; + } } + pretty = fastargs[4]; skip_optional_kwonly: objects = args; objects_length = nargs; - return_value = builtin_print_impl(module, objects, objects_length, sep, end, file, flush); + return_value = builtin_print_impl(module, objects, objects_length, sep, end, file, flush, pretty); exit: return return_value; @@ -1277,4 +1287,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs) exit: return return_value; } -/*[clinic end generated code: output=7eada753dc2e046f input=a9049054013a1b77]*/ +/*[clinic end generated code: output=45f6ee7f6eb4bd75 input=a9049054013a1b77]*/ From 313ccd19db2bc111c16b8629d22be980048a0307 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 14:23:02 -0700 Subject: [PATCH 13/30] Kinda works, at least for passing in an explicit `pretty` object. pretty=True doesn't work yet. --- Python/bltinmodule.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index ba86828aeeb7e7..eace5e51aac37f 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2270,6 +2270,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); PyObject *printer = NULL; + PyObject_Print(printer_factory, stderr, 0); if (!printer_factory) { Py_DECREF(file); return NULL; @@ -2281,6 +2282,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } + PyObject_Print(printer, stderr, 0); } else if (pretty == Py_None) { /* Don't use a pretty printer */ @@ -2305,7 +2307,33 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, return NULL; } } - err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); + /* XXX: I have a couple of thoughts about how this could be handled. We could add a + PyFile_WriteObjectEx() function which would look largely like PyFile_WriteObject() but + would take a pretty printer object (or None, in which case it would just fall back to + PyFile_WriteObject()). Then we could put the logic for the (TBD) "pretty printing + protocol" in there. + + For now though, let's keep things localized so all the logic is in the print() function's + implementation. Maybe a better way will come to mind as we pan this idea out. + + Or, this currently calls `printer.pformat(object)` so a pretty printing protocol could + be implemented there. Or maybe we want a more generic method name. + */ + PyObject_Print(printer, stderr, 0); + if (printer) { + PyObject *prettified = PyObject_CallMethod(printer, "pformat", "O", objects[i]); + + if (!prettified) { + Py_DECREF(file); + Py_DECREF(printer); + return NULL; + } + err = PyFile_WriteObject(prettified, file, Py_PRINT_RAW); + Py_XDECREF(prettified); + } + else { + err = PyFile_WriteObject(objects[i], file, Py_PRINT_RAW); + } if (err) { Py_DECREF(file); Py_XDECREF(printer); From d6766c6b15823059822fe1ab273a4ac51c2b1bc7 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 15:23:09 -0700 Subject: [PATCH 14/30] Fix pretty=True and remove debugging --- Python/bltinmodule.c | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index eace5e51aac37f..55568e58dcdd8a 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2268,9 +2268,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, if (pretty == Py_True) { /* Use default `pprint.PrettyPrinter` */ PyObject *printer_factory = PyImport_ImportModuleAttrString("pprint", "PrettyPrinter"); - PyObject *printer = NULL; - PyObject_Print(printer_factory, stderr, 0); if (!printer_factory) { Py_DECREF(file); return NULL; @@ -2282,7 +2280,6 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Py_DECREF(file); return NULL; } - PyObject_Print(printer, stderr, 0); } else if (pretty == Py_None) { /* Don't use a pretty printer */ @@ -2319,7 +2316,6 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, Or, this currently calls `printer.pformat(object)` so a pretty printing protocol could be implemented there. Or maybe we want a more generic method name. */ - PyObject_Print(printer, stderr, 0); if (printer) { PyObject *prettified = PyObject_CallMethod(printer, "pformat", "O", objects[i]); From 71dcfe7f77b5b6c7025b10e9418f7c0fe78c2fd2 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 26 Oct 2025 08:41:33 -0700 Subject: [PATCH 15/30] Call object's __pprint__() function if it has one --- Lib/pprint.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Lib/pprint.py b/Lib/pprint.py index 92a2c543ac279c..fabc90907e1d00 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -615,6 +615,9 @@ def _safe_repr(self, object, context, maxlevels, level): if typ in _builtin_scalars: return repr(object), True, False + if (p := getattr(typ, "__pprint__", None)): + return p(object, context, maxlevels, level), True, False + r = getattr(typ, "__repr__", None) if issubclass(typ, int) and r is int.__repr__: From 4d237da35dac5e573a2f443ef8e8272e47a2b425 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 27 Oct 2025 20:30:15 -0700 Subject: [PATCH 16/30] Add some print(..., pretty=) tests --- Lib/test/test_print.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index 12256b3b562637..e8898f54b61edd 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -1,6 +1,7 @@ import unittest import sys from io import StringIO +from pprint import PrettyPrinter from test import support @@ -200,5 +201,44 @@ def test_string_in_loop_on_same_line(self): str(context.exception)) +class PPrintable: + def __pprint__(self, context, maxlevels, level): + return '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' 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 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() From bb23638636810aadcc01134bc3fd046dac183b51 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 11:31:45 -0800 Subject: [PATCH 17/30] Flesh out the pprint protocol documentation * Update the docs on the `print()` function to describe the new `pretty` keyword * Document the `__pprint__()` protocol. * Add a test for the `__pprint__()` protocol method. --- Doc/library/functions.rst | 27 ++++++++++++++++++--------- Doc/library/pprint.rst | 22 ++++++++++++++++++++++ Lib/test/test_pprint.py | 19 +++++++++++++++++++ 3 files changed, 59 insertions(+), 9 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 61799e303a1639..b03033219fbe14 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1587,17 +1587,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 + :method:`PrettyPrinter.pprint()` API which takes an object and returns a + formatted representation of the object. When *pretty* is ``True``, then it + actually does call ``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 @@ -1611,6 +1617,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) diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst index f51892450798ae..5a6af7acec0195 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -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__ ` protocol. + .. _pprint-functions: Functions @@ -253,6 +256,16 @@ 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 will use 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 with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format()`. The +``__pprint__()`` method is expected to return a string, which is used as the pretty printed representation of +the object. .. _pprint-example: @@ -418,3 +431,12 @@ cannot be split, the specified width will be exceeded:: 'requires_python': None, 'summary': 'A sample Python project', 'version': '1.2.0'} + +A custom ``__pprint__()`` method can be used to customize the representation of the object:: + + >>> class Custom: + ... def __str__(self): return 'my str' + ... def __repr__(self): return 'my repr' + ... def __pprint__(self, context, maxlevels, level): return 'my pprint' + >>> pprint.pp(Custom()) + my pprint diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 41c337ade7eca1..daa1dbd8c91ab4 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -134,6 +134,18 @@ def __ne__(self, other): def __hash__(self): return self._hash + +class CustomPrintable: + def __str__(self): + return "my str" + + def __repr__(self): + return "my str" + + def __pprint__(self, context, maxlevels, level): + return "my pprint" + + class QueryTestCase(unittest.TestCase): def setUp(self): @@ -1472,6 +1484,13 @@ def test_user_string(self): 'jumped over a ' 'lazy dog'}""") + def test_custom_pprinter(self): + stream = io.StringIO() + pp = pprint.PrettyPrinter(stream=stream) + custom_obj = CustomPrintable() + pp.pprint(custom_obj) + self.assertEqual(stream.getvalue(), "my pprint\n") + class DottedPrettyPrinter(pprint.PrettyPrinter): From 3965667cc4e59be04b9e7da50eecd14b3b260205 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 16:41:29 -0800 Subject: [PATCH 18/30] The pre-PEP --- pep-9999.rst | 154 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 pep-9999.rst diff --git a/pep-9999.rst b/pep-9999.rst new file mode 100644 index 00000000000000..af0aadf21f488e --- /dev/null +++ b/pep-9999.rst @@ -0,0 +1,154 @@ +PEP: 9999 +Title: The pprint protocol +Author: Barry Warsaw , + Eric V. Smith +Discussions-To: Pending +Status: Draft +Type: Standards Track +Created: 03-Nov-2025 +Python-Version: 3.15 +Post-History: Pending + + +Abstract +======== + +This PEP describes the "pprint protocol", a collection of changes proposed to make pretty printing more +customizable and convenient. + + +Motivation +========== + +"Pretty printing" is a feature which provides a capability to format object representations for better +readability. The core functionality is implemented by the standard library :mod:`pprint`. ``pprint`` +includes a class and APIs which users can invoke to format and print more readable representations of objects. +Important use cases include pretty printing large dictionaries and other complicated objects. + +The ``pprint`` module is great as far as it goes. This PEP builds on the features of this module to provide +more customization and convenience. + + +Rationale +========= + +Pretty printing is very useful for displaying complex data structures, like dictionaries read from JSON +content. By providing a way for classes to customize how their instances participate in pretty printing, +users have more options for visually improving the display and debugging of their complex data. + +By extending the built-in :py:func:`print` function to automatically pretty print its output, this feature is +made even more convenient, since no extra imports are required, and users can easily just piggyback on +well-worn "print debugging" patterns, at least for the most common use cases. + +These two extensions work independently, but hand-in-hand can provide a powerful and convenient new feature. + + +Specification +============= + +There are two parts to this proposal. + + +``__pretty__()`` methods +------------------------ + +Classes can implement a new dunder method, ``__pretty__()`` which if present, generates the pretty printed +representation of their instances. This augments ``__repr__()`` which, prior to this proposal, was the only +method used to generate a pretty representation of the object. Since the built-in :py:func:`repr` function +provides functionality potentially separate from pretty printing, some classes may want more control over +object display between those two use cases. + +``__pretty__()`` is optional; if missing, the standard pretty printers fall back to ``__repr__()`` for full +backward compatibility. However, if defined on a class, ``__pretty__()`` has the same argument signature as +:py:func:`PrettyPrinter.format`, taking four arguments: + +* ``object`` - the object to print, which is effectively always ``self`` +* ``context`` - a dictionary mapping the ``id()`` of objects which are part of the current presentation + context +* ``maxlevels`` - the requested limit to recursion +* ``levels`` - the current recursion level + +See :py:func:`PrettyPrinter.format` for details. + +Unlike that function, ``__pretty__()`` returns a single value, the string to be used as the pretty printed +representation. + + +A new argument to built-in ``print`` +------------------------------------ + +Built-in :py:func:`print` takes a new optional argument, appended to the end of the argument list, called +``pretty``, which can take one of the following values: + +* ``None`` - the default; fully backward compatible +* ``True`` - use a temporary instance of the :py:class:`PrettyPrinter` class to get a pretty representation of + the object. +* An instance with a ``pformat()`` method, which has the same signature as + :meth:`PrettyPrinter.pformat`. When given, this will usually be an instance of a subclass of + `PrettyPrinter` with its `pformat()` method overridden. Note that this form requires **an + instance** of a pretty printer, not a class, as only ``print(..., pretty=True)`` performs implicit + instantiation. + + +Backwards Compatibility +======================= + +When none of the new features are used, this PEP is fully backward compatible, both for built-in +``print()`` and the ``pprint`` module. + + +Security Implications +===================== + +There are no known security implications for this proposal. + + +How to Teach This +================= + +Documentation and examples are added to the ``pprint`` module and the ``print()`` function. +Beginners don't need to be taught these new features until they want prettier representations of +their objects. + + +Reference Implementation +======================== + +The reference implementation is currently available as a `PEP author branch of the CPython main +branch `__. + + +Rejected Ideas +============== + +None at this time. + + +Open Issues +=========== + +As currently defined, the ``__pretty__()`` method is defined as taking four arguments and returning +a single string. This is close to -- but not quite -- the full signature for +``PrettyPrinter.format()``, and was chosen for reference implementation convenience. It's not +clear that custom pretty representations need ``context``, ``maxlevels``, and ``levels``, so perhaps +the argument list should be simplified? If not, then perhaps ``__pretty__()`` should *exactly* +match ``PrettyPrinter.format()``? That does, however complicate the protocol for users. + + +Acknowledgements +================ + +TBD + + +Footnotes +========= + +TBD + + +Copyright +========= + +This document is placed in the public domain or under the +CC0-1.0-Universal license, whichever is more permissive. From 3e20e374dc5151fcb35ce13c043b731b7007ed0e Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 16:56:15 -0800 Subject: [PATCH 19/30] Fix a doc lint warning --- Doc/library/pprint.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst index 5a6af7acec0195..4ef7a7664477a1 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -263,7 +263,7 @@ The "__pprint__" protocol Pretty printing will use 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 with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format()`. The +exists, it is called with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format`. The ``__pprint__()`` method is expected to return a string, which is used as the pretty printed representation of the object. From 7786ec152fcd11e7e48fcad148b62d11217af18c Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 3 Nov 2025 18:11:46 -0800 Subject: [PATCH 20/30] Add some examples --- pep-9999.rst | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/pep-9999.rst b/pep-9999.rst index af0aadf21f488e..dc2b96f0e9e2b8 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -90,6 +90,51 @@ Built-in :py:func:`print` takes a new optional argument, appended to the end of instantiation. +Examples +======== + +A custom ``__pprint__()`` method can be used to customize the representation of the object: + +.. _code-block: + + >>> class Custom: + ... def __str__(self): return 'my str' + ... def __repr__(self): return 'my repr' + ... def __pprint__(self, context, maxlevels, level): return 'my pprint' + + >>> pprint.pp(Custom()) + my pprint + +Using the ``pretty`` argument to ``print()``: + +.. _code-block: + + >>> import os + >>> print(os.pathconf_names) + {'PC_ASYNC_IO': 17, 'PC_CHOWN_RESTRICTED': 7, 'PC_FILESIZEBITS': 18, 'PC_LINK_MAX': 1, 'PC_MAX_CANON': 2, 'PC_MAX_INPUT': 3, 'PC_NAME_MAX': 4, 'PC_NO_TRUNC': 8, 'PC_PATH_MAX': 5, 'PC_PIPE_BUF': 6, 'PC_PRIO_IO': 19, 'PC_SYNC_IO': 25, 'PC_VDISABLE': 9, 'PC_MIN_HOLE_SIZE': 27, 'PC_ALLOC_SIZE_MIN': 16, 'PC_REC_INCR_XFER_SIZE': 20, 'PC_REC_MAX_XFER_SIZE': 21, 'PC_REC_MIN_XFER_SIZE': 22, 'PC_REC_XFER_ALIGN': 23, 'PC_SYMLINK_MAX': 24} + >>> print(os.pathconf_names, pretty=True) + {'PC_ALLOC_SIZE_MIN': 16, + 'PC_ASYNC_IO': 17, + 'PC_CHOWN_RESTRICTED': 7, + 'PC_FILESIZEBITS': 18, + 'PC_LINK_MAX': 1, + 'PC_MAX_CANON': 2, + 'PC_MAX_INPUT': 3, + 'PC_MIN_HOLE_SIZE': 27, + 'PC_NAME_MAX': 4, + 'PC_NO_TRUNC': 8, + 'PC_PATH_MAX': 5, + 'PC_PIPE_BUF': 6, + 'PC_PRIO_IO': 19, + 'PC_REC_INCR_XFER_SIZE': 20, + 'PC_REC_MAX_XFER_SIZE': 21, + 'PC_REC_MIN_XFER_SIZE': 22, + 'PC_REC_XFER_ALIGN': 23, + 'PC_SYMLINK_MAX': 24, + 'PC_SYNC_IO': 25, + 'PC_VDISABLE': 9} + + Backwards Compatibility ======================= From 506070132e1b395194179b1147392c1f1a23aa4e Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 5 Nov 2025 13:45:02 -0800 Subject: [PATCH 21/30] __pretty__() is now exactly signatured as PrettyPrinter.format() --- Lib/pprint.py | 2 +- Lib/test/test_pprint.py | 3 ++- Lib/test/test_print.py | 2 +- pep-9999.rst | 26 +++++++++++--------------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index fabc90907e1d00..3b8990dc7f410c 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -616,7 +616,7 @@ def _safe_repr(self, object, context, maxlevels, level): return repr(object), True, False if (p := getattr(typ, "__pprint__", None)): - return p(object, context, maxlevels, level), True, False + return p(object, context, maxlevels, level) r = getattr(typ, "__repr__", None) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index daa1dbd8c91ab4..61d9fa72358f09 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -143,7 +143,8 @@ def __repr__(self): return "my str" def __pprint__(self, context, maxlevels, level): - return "my pprint" + # The custom pretty repr, not-readable bool, no recursion detected. + return "my pprint", False, False class QueryTestCase(unittest.TestCase): diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index e8898f54b61edd..b13380d354e468 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -203,7 +203,7 @@ def test_string_in_loop_on_same_line(self): class PPrintable: def __pprint__(self, context, maxlevels, level): - return 'I feel pretty' + return 'I feel pretty', False, False class PrettySmart(PrettyPrinter): diff --git a/pep-9999.rst b/pep-9999.rst index dc2b96f0e9e2b8..12ca97c2c29d03 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -54,13 +54,13 @@ There are two parts to this proposal. Classes can implement a new dunder method, ``__pretty__()`` which if present, generates the pretty printed representation of their instances. This augments ``__repr__()`` which, prior to this proposal, was the only -method used to generate a pretty representation of the object. Since the built-in :py:func:`repr` function -provides functionality potentially separate from pretty printing, some classes may want more control over -object display between those two use cases. +method used to generate a pretty representation of the object. Since object reprs provide functionality +distinct from pretty printing, some classes may want more control over their pretty display. ``__pretty__()`` is optional; if missing, the standard pretty printers fall back to ``__repr__()`` for full -backward compatibility. However, if defined on a class, ``__pretty__()`` has the same argument signature as -:py:func:`PrettyPrinter.format`, taking four arguments: +backward compatibility (technically speaking, :meth:`pprint.saferepr` is used). However, if defined on a +class, ``__pretty__()`` has the same argument signature as :py:func:`PrettyPrinter.format`, taking four +arguments: * ``object`` - the object to print, which is effectively always ``self`` * ``context`` - a dictionary mapping the ``id()`` of objects which are part of the current presentation @@ -68,10 +68,12 @@ backward compatibility. However, if defined on a class, ``__pretty__()`` has th * ``maxlevels`` - the requested limit to recursion * ``levels`` - the current recursion level -See :py:func:`PrettyPrinter.format` for details. +Similarly, ``__pretty__()`` returns three values, the string to be used as the pretty printed representation, +a boolean indicating whether the returned value is "readable", and a boolean indicating whether recursion has +been detected. In this context, "readable" means the same as :meth:`PrettyPrinter.isreadable`, i.e. that the +returned value can be used to reconstruct the original object using ``eval()``. -Unlike that function, ``__pretty__()`` returns a single value, the string to be used as the pretty printed -representation. +See :py:func:`PrettyPrinter.format` for details. A new argument to built-in ``print`` @@ -172,13 +174,7 @@ None at this time. Open Issues =========== -As currently defined, the ``__pretty__()`` method is defined as taking four arguments and returning -a single string. This is close to -- but not quite -- the full signature for -``PrettyPrinter.format()``, and was chosen for reference implementation convenience. It's not -clear that custom pretty representations need ``context``, ``maxlevels``, and ``levels``, so perhaps -the argument list should be simplified? If not, then perhaps ``__pretty__()`` should *exactly* -match ``PrettyPrinter.format()``? That does, however complicate the protocol for users. - +TBD Acknowledgements ================ From 41cd66709684641cf1142fd7e307111f33361499 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 5 Nov 2025 14:45:01 -0800 Subject: [PATCH 22/30] Title --- pep-9999.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pep-9999.rst b/pep-9999.rst index 12ca97c2c29d03..4f0b086a63873a 100644 --- a/pep-9999.rst +++ b/pep-9999.rst @@ -1,5 +1,5 @@ PEP: 9999 -Title: The pprint protocol +Title: The Pretty Print Protocol Author: Barry Warsaw , Eric V. Smith Discussions-To: Pending @@ -13,7 +13,7 @@ Post-History: Pending Abstract ======== -This PEP describes the "pprint protocol", a collection of changes proposed to make pretty printing more +This PEP describes the "pretty print protocol", a collection of changes proposed to make pretty printing more customizable and convenient. From 5c6872fd60254e216c8f9ee4002daf4c41bde605 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 7 Nov 2025 09:23:26 -0800 Subject: [PATCH 23/30] PEP submitted: https://github.com/python/peps/pull/4690 --- pep-9999.rst | 195 --------------------------------------------------- 1 file changed, 195 deletions(-) delete mode 100644 pep-9999.rst diff --git a/pep-9999.rst b/pep-9999.rst deleted file mode 100644 index 4f0b086a63873a..00000000000000 --- a/pep-9999.rst +++ /dev/null @@ -1,195 +0,0 @@ -PEP: 9999 -Title: The Pretty Print Protocol -Author: Barry Warsaw , - Eric V. Smith -Discussions-To: Pending -Status: Draft -Type: Standards Track -Created: 03-Nov-2025 -Python-Version: 3.15 -Post-History: Pending - - -Abstract -======== - -This PEP describes the "pretty print protocol", a collection of changes proposed to make pretty printing more -customizable and convenient. - - -Motivation -========== - -"Pretty printing" is a feature which provides a capability to format object representations for better -readability. The core functionality is implemented by the standard library :mod:`pprint`. ``pprint`` -includes a class and APIs which users can invoke to format and print more readable representations of objects. -Important use cases include pretty printing large dictionaries and other complicated objects. - -The ``pprint`` module is great as far as it goes. This PEP builds on the features of this module to provide -more customization and convenience. - - -Rationale -========= - -Pretty printing is very useful for displaying complex data structures, like dictionaries read from JSON -content. By providing a way for classes to customize how their instances participate in pretty printing, -users have more options for visually improving the display and debugging of their complex data. - -By extending the built-in :py:func:`print` function to automatically pretty print its output, this feature is -made even more convenient, since no extra imports are required, and users can easily just piggyback on -well-worn "print debugging" patterns, at least for the most common use cases. - -These two extensions work independently, but hand-in-hand can provide a powerful and convenient new feature. - - -Specification -============= - -There are two parts to this proposal. - - -``__pretty__()`` methods ------------------------- - -Classes can implement a new dunder method, ``__pretty__()`` which if present, generates the pretty printed -representation of their instances. This augments ``__repr__()`` which, prior to this proposal, was the only -method used to generate a pretty representation of the object. Since object reprs provide functionality -distinct from pretty printing, some classes may want more control over their pretty display. - -``__pretty__()`` is optional; if missing, the standard pretty printers fall back to ``__repr__()`` for full -backward compatibility (technically speaking, :meth:`pprint.saferepr` is used). However, if defined on a -class, ``__pretty__()`` has the same argument signature as :py:func:`PrettyPrinter.format`, taking four -arguments: - -* ``object`` - the object to print, which is effectively always ``self`` -* ``context`` - a dictionary mapping the ``id()`` of objects which are part of the current presentation - context -* ``maxlevels`` - the requested limit to recursion -* ``levels`` - the current recursion level - -Similarly, ``__pretty__()`` returns three values, the string to be used as the pretty printed representation, -a boolean indicating whether the returned value is "readable", and a boolean indicating whether recursion has -been detected. In this context, "readable" means the same as :meth:`PrettyPrinter.isreadable`, i.e. that the -returned value can be used to reconstruct the original object using ``eval()``. - -See :py:func:`PrettyPrinter.format` for details. - - -A new argument to built-in ``print`` ------------------------------------- - -Built-in :py:func:`print` takes a new optional argument, appended to the end of the argument list, called -``pretty``, which can take one of the following values: - -* ``None`` - the default; fully backward compatible -* ``True`` - use a temporary instance of the :py:class:`PrettyPrinter` class to get a pretty representation of - the object. -* An instance with a ``pformat()`` method, which has the same signature as - :meth:`PrettyPrinter.pformat`. When given, this will usually be an instance of a subclass of - `PrettyPrinter` with its `pformat()` method overridden. Note that this form requires **an - instance** of a pretty printer, not a class, as only ``print(..., pretty=True)`` performs implicit - instantiation. - - -Examples -======== - -A custom ``__pprint__()`` method can be used to customize the representation of the object: - -.. _code-block: - - >>> class Custom: - ... def __str__(self): return 'my str' - ... def __repr__(self): return 'my repr' - ... def __pprint__(self, context, maxlevels, level): return 'my pprint' - - >>> pprint.pp(Custom()) - my pprint - -Using the ``pretty`` argument to ``print()``: - -.. _code-block: - - >>> import os - >>> print(os.pathconf_names) - {'PC_ASYNC_IO': 17, 'PC_CHOWN_RESTRICTED': 7, 'PC_FILESIZEBITS': 18, 'PC_LINK_MAX': 1, 'PC_MAX_CANON': 2, 'PC_MAX_INPUT': 3, 'PC_NAME_MAX': 4, 'PC_NO_TRUNC': 8, 'PC_PATH_MAX': 5, 'PC_PIPE_BUF': 6, 'PC_PRIO_IO': 19, 'PC_SYNC_IO': 25, 'PC_VDISABLE': 9, 'PC_MIN_HOLE_SIZE': 27, 'PC_ALLOC_SIZE_MIN': 16, 'PC_REC_INCR_XFER_SIZE': 20, 'PC_REC_MAX_XFER_SIZE': 21, 'PC_REC_MIN_XFER_SIZE': 22, 'PC_REC_XFER_ALIGN': 23, 'PC_SYMLINK_MAX': 24} - >>> print(os.pathconf_names, pretty=True) - {'PC_ALLOC_SIZE_MIN': 16, - 'PC_ASYNC_IO': 17, - 'PC_CHOWN_RESTRICTED': 7, - 'PC_FILESIZEBITS': 18, - 'PC_LINK_MAX': 1, - 'PC_MAX_CANON': 2, - 'PC_MAX_INPUT': 3, - 'PC_MIN_HOLE_SIZE': 27, - 'PC_NAME_MAX': 4, - 'PC_NO_TRUNC': 8, - 'PC_PATH_MAX': 5, - 'PC_PIPE_BUF': 6, - 'PC_PRIO_IO': 19, - 'PC_REC_INCR_XFER_SIZE': 20, - 'PC_REC_MAX_XFER_SIZE': 21, - 'PC_REC_MIN_XFER_SIZE': 22, - 'PC_REC_XFER_ALIGN': 23, - 'PC_SYMLINK_MAX': 24, - 'PC_SYNC_IO': 25, - 'PC_VDISABLE': 9} - - -Backwards Compatibility -======================= - -When none of the new features are used, this PEP is fully backward compatible, both for built-in -``print()`` and the ``pprint`` module. - - -Security Implications -===================== - -There are no known security implications for this proposal. - - -How to Teach This -================= - -Documentation and examples are added to the ``pprint`` module and the ``print()`` function. -Beginners don't need to be taught these new features until they want prettier representations of -their objects. - - -Reference Implementation -======================== - -The reference implementation is currently available as a `PEP author branch of the CPython main -branch `__. - - -Rejected Ideas -============== - -None at this time. - - -Open Issues -=========== - -TBD - -Acknowledgements -================ - -TBD - - -Footnotes -========= - -TBD - - -Copyright -========= - -This document is placed in the public domain or under the -CC0-1.0-Universal license, whichever is more permissive. From 28fa67d38845f49f7f44d153b1022d2a15340650 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Fri, 7 Nov 2025 13:30:21 -0800 Subject: [PATCH 24/30] Remove an obsolete comment --- Python/bltinmodule.c | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c index 55568e58dcdd8a..ba477d919dbd0c 100644 --- a/Python/bltinmodule.c +++ b/Python/bltinmodule.c @@ -2304,18 +2304,7 @@ builtin_print_impl(PyObject *module, PyObject * const *objects, return NULL; } } - /* XXX: I have a couple of thoughts about how this could be handled. We could add a - PyFile_WriteObjectEx() function which would look largely like PyFile_WriteObject() but - would take a pretty printer object (or None, in which case it would just fall back to - PyFile_WriteObject()). Then we could put the logic for the (TBD) "pretty printing - protocol" in there. - - For now though, let's keep things localized so all the logic is in the print() function's - implementation. Maybe a better way will come to mind as we pan this idea out. - - Or, this currently calls `printer.pformat(object)` so a pretty printing protocol could - be implemented there. Or maybe we want a more generic method name. - */ + if (printer) { PyObject *prettified = PyObject_CallMethod(printer, "pformat", "O", objects[i]); From 055eda77408d98f64342695052af7c4a3ccb11ee Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 14 Dec 2025 11:45:32 -0800 Subject: [PATCH 25/30] Use a rich.pretty compatible pretty printer API --- Lib/pprint.py | 38 +++++++++++++++++++- Lib/test/test_pprint.py | 77 ++++++++++++++++++++++++++++++++++++++--- Lib/test/test_print.py | 8 ++--- 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 3b8990dc7f410c..42f7a1453f03a0 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -609,6 +609,42 @@ 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): + if isinstance(item, tuple): + if len(item) == 2: + # (name, value) - always show + name, value = item + vrep, vreadable, _ = self.format(value, context, maxlevels, level + 1) + parts.append(f"{name}={vrep}") + readable = readable and vreadable + elif len(item) == 3: + # (name, value, default) - show only if value != default + name, value, default = item + if value != default: + vrep, vreadable, _ = self.format(value, context, maxlevels, level + 1) + parts.append(f"{name}={vrep}") + readable = readable and vreadable + else: + # Positional argument + vrep, vreadable, _ = self.format(item, context, maxlevels, level + 1) + parts.append(vrep) + readable = readable and vreadable + + 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) @@ -616,7 +652,7 @@ def _safe_repr(self, object, context, maxlevels, level): return repr(object), True, False if (p := getattr(typ, "__pprint__", None)): - return p(object, context, maxlevels, level) + return self._format_pprint(object, p, context, maxlevels, level) r = getattr(typ, "__repr__", None) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index 61d9fa72358f09..fdadc7e2388f10 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -136,15 +136,19 @@ def __hash__(self): class CustomPrintable: + def __init__(self, name="my pprint", value=42): + self.name = name + self.value = value + def __str__(self): return "my str" def __repr__(self): return "my str" - def __pprint__(self, context, maxlevels, level): - # The custom pretty repr, not-readable bool, no recursion detected. - return "my pprint", False, False + def __pprint__(self): + yield self.name + yield ("value", self.value) class QueryTestCase(unittest.TestCase): @@ -1490,7 +1494,72 @@ def test_custom_pprinter(self): pp = pprint.PrettyPrinter(stream=stream) custom_obj = CustomPrintable() pp.pprint(custom_obj) - self.assertEqual(stream.getvalue(), "my pprint\n") + 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 + self.assertEqual(pprint.pformat(Point(1, 2)), "Point(1, 2)") + + 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) + self.assertEqual(pprint.pformat(Config("localhost", 8080)), + "Config(host='localhost', port=8080)") + + def test_pprint_protocol_default(self): + # Test __pprint__ with default values (3-tuple form) + class Bird: + def __init__(self, name, fly=True, extinct=False): + self.name = name + self.fly = fly + self.extinct = extinct + def __pprint__(self): + yield self.name + yield ("fly", self.fly, True) # hide if True + yield ("extinct", self.extinct, False) # hide if False + + # Defaults should be hidden + self.assertEqual(pprint.pformat(Bird("sparrow")), + "Bird('sparrow')") + # Non-defaults should be shown + self.assertEqual(pprint.pformat(Bird("dodo", fly=False, extinct=True)), + "Bird('dodo', fly=False, extinct=True)") + + 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) + c = Container([1, 2, 3]) + self.assertEqual(pprint.pformat(c), "Container(items=[1, 2, 3])") + # Nested in a list + self.assertEqual(pprint.pformat([c]), "[Container(items=[1, 2, 3])]") + + 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): diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index b13380d354e468..cd3139837d4dee 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -202,8 +202,8 @@ def test_string_in_loop_on_same_line(self): class PPrintable: - def __pprint__(self, context, maxlevels, level): - return 'I feel pretty', False, False + def __pprint__(self): + yield 'I feel pretty' class PrettySmart(PrettyPrinter): @@ -229,11 +229,11 @@ def test_default_pretty_printer(self): def test_pprint_magic(self): print('one', PPrintable(), 2, file=self.file, pretty=True) - self.assertEqual(self.file.getvalue(), "'one' I feel pretty 2\n") + 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 I feel pretty 2\n") + self.assertEqual(self.file.getvalue(), "one PPrintable('I feel pretty') 2\n") def test_bad_pprinter(self): with self.assertRaises(AttributeError): From 840e9611737706d3bf197e146f2699ba88005208 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 22 Dec 2025 12:06:44 -0800 Subject: [PATCH 26/30] Update the pretty print protocol documentation --- Doc/library/functions.rst | 4 +-- Doc/library/pprint.rst | 66 ++++++++++++++++++++++++++++++++------- 2 files changed, 57 insertions(+), 13 deletions(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 246295d921dec8..2b310c53da135b 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1610,9 +1610,9 @@ are always available. They are listed here in alphabetical order. When *pretty* is given, it signals that the objects should be "pretty printed". *pretty* can be ``True`` or an object implementing the - :method:`PrettyPrinter.pprint()` API which takes an object and returns a + :meth:`PrettyPrinter.pprint()` API which takes an object and returns a formatted representation of the object. When *pretty* is ``True``, then it - actually does call ``PrettyPrinter.pformat()`` explicitly. + 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 diff --git a/Doc/library/pprint.rst b/Doc/library/pprint.rst index 4ef7a7664477a1..77aea3e37e3b0c 100644 --- a/Doc/library/pprint.rst +++ b/Doc/library/pprint.rst @@ -261,11 +261,29 @@ are converted to strings. The default implementation uses the internals of the The "__pprint__" protocol ------------------------- -Pretty printing will use an object's ``__repr__`` by default. For custom pretty printing, objects can +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 with 4 arguments, exactly matching the API of :meth:`PrettyPrinter.format`. The -``__pprint__()`` method is expected to return a string, which is used as the pretty printed representation of -the object. +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 + `_. + +See the :ref:`pprint-protocol-example` for how this can be used in practice. .. _pprint-example: @@ -432,11 +450,37 @@ cannot be split, the specified width will be exceeded:: 'summary': 'A sample Python project', 'version': '1.2.0'} -A custom ``__pprint__()`` method can be used to customize the representation of the object:: +.. _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) - >>> class Custom: - ... def __str__(self): return 'my str' - ... def __repr__(self): return 'my repr' - ... def __pprint__(self, context, maxlevels, level): return 'my pprint' - >>> pprint.pp(Custom()) - my pprint +Note that you'd get exactly the same output if you used ``print(..., pretty=True)``. From 30738919a782524d45f4d8f65ab86171541b60f7 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 22 Dec 2025 13:16:22 -0800 Subject: [PATCH 27/30] Use a match statement in _format_pprint() --- Lib/pprint.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index 42f7a1453f03a0..54aa98a6b7de4f 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -622,25 +622,23 @@ def _format_pprint(self, object, method, context, maxlevels, level): readable = True for item in method(object): - if isinstance(item, tuple): - if len(item) == 2: - # (name, value) - always show - name, value = item - vrep, vreadable, _ = self.format(value, context, maxlevels, level + 1) - parts.append(f"{name}={vrep}") - readable = readable and vreadable - elif len(item) == 3: - # (name, value, default) - show only if value != default - name, value, default = item + match item: + case (name, value, default): + # Keyword argument w/default. Show only if value != default. if value != default: - vrep, vreadable, _ = self.format(value, context, maxlevels, level + 1) - parts.append(f"{name}={vrep}") - readable = readable and vreadable - else: - # Positional argument - vrep, vreadable, _ = self.format(item, context, maxlevels, level + 1) - parts.append(vrep) - readable = readable and vreadable + 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 From 21734be0399c45adf6973c0002fc7f0f9a912073 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 22 Dec 2025 13:33:13 -0800 Subject: [PATCH 28/30] Improve the pretty print test protocol --- Lib/test/test_pprint.py | 85 ++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index fdadc7e2388f10..e6ac761df28fa6 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -135,22 +135,6 @@ def __hash__(self): return self._hash -class CustomPrintable: - def __init__(self, name="my pprint", value=42): - self.name = name - self.value = value - - def __str__(self): - return "my str" - - def __repr__(self): - return "my str" - - def __pprint__(self): - yield self.name - yield ("value", self.value) - - class QueryTestCase(unittest.TestCase): def setUp(self): @@ -1490,10 +1474,18 @@ def test_user_string(self): '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() - pp = pprint.PrettyPrinter(stream=stream) - custom_obj = CustomPrintable() - pp.pprint(custom_obj) + pprint.pprint(CustomPrintable(), stream=stream) self.assertEqual(stream.getvalue(), "CustomPrintable('my pprint', value=42)\n") def test_pprint_protocol_positional(self): @@ -1505,7 +1497,10 @@ def __init__(self, x, y): def __pprint__(self): yield self.x yield self.y - self.assertEqual(pprint.pformat(Point(1, 2)), "Point(1, 2)") + + 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 @@ -1516,39 +1511,49 @@ def __init__(self, host, port): def __pprint__(self): yield ("host", self.host) yield ("port", self.port) - self.assertEqual(pprint.pformat(Config("localhost", 8080)), - "Config(host='localhost', port=8080)") + + 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 Bird: - def __init__(self, name, fly=True, extinct=False): - self.name = name - self.fly = fly - self.extinct = extinct + 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.name - yield ("fly", self.fly, True) # hide if True - yield ("extinct", self.extinct, False) # hide if False + yield self._strings + yield 'pickups', self._pickups + yield 'active', self._active, False - # Defaults should be hidden - self.assertEqual(pprint.pformat(Bird("sparrow")), - "Bird('sparrow')") - # Non-defaults should be shown - self.assertEqual(pprint.pformat(Bird("dodo", fly=False, extinct=True)), - "Bird('dodo', fly=False, extinct=True)") + # 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 + # Test __pprint__ with nested objects. class Container: def __init__(self, items): self.items = items def __pprint__(self): - yield ("items", self.items) + yield "items", self.items + + stream = io.StringIO() c = Container([1, 2, 3]) - self.assertEqual(pprint.pformat(c), "Container(items=[1, 2, 3])") + pprint.pprint(c, stream=stream) + self.assertEqual(stream.getvalue(), "Container(items=[1, 2, 3])\n") # Nested in a list - self.assertEqual(pprint.pformat([c]), "[Container(items=[1, 2, 3])]") + 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__ From 3aaa402355079c953c056edea8d767a9073a7929 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Tue, 17 Feb 2026 11:20:06 -0800 Subject: [PATCH 29/30] Fix doc lint error --- Doc/library/functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index e6990ab1ef5040..625d332b381ebd 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1612,7 +1612,7 @@ are always available. They are listed here in alphabetical order. When *pretty* is given, it signals that the objects should be "pretty printed". *pretty* can be ``True`` or an object implementing the - :meth:`PrettyPrinter.pprint()` API which takes an object and returns a + :meth:`PrettyPrinter.pprint` API which takes an object and returns a formatted representation of the object. When *pretty* is ``True``, then it calls ``PrettyPrinter.pformat()`` explicitly. From 7fc13b2b71e0962722909c32000ed552c68aaf26 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Tue, 17 Feb 2026 14:31:44 -0800 Subject: [PATCH 30/30] Fix a cross-reference --- Doc/library/functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst index 625d332b381ebd..45ba7f18649e94 100644 --- a/Doc/library/functions.rst +++ b/Doc/library/functions.rst @@ -1612,7 +1612,7 @@ are always available. They are listed here in alphabetical order. When *pretty* is given, it signals that the objects should be "pretty printed". *pretty* can be ``True`` or an object implementing the - :meth:`PrettyPrinter.pprint` API which takes an object and returns a + :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.