From cbb0b0a65206c169e5996667c42e66fe01dbbddf Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sat, 25 Oct 2025 13:47:24 -0700 Subject: [PATCH 01/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] __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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] __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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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/37] 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. From 575e3f0263a8e12f4683a8b9025ed6025666b439 Mon Sep 17 00:00:00 2001 From: "Eric V. Smith" Date: Sun, 22 Feb 2026 17:09:39 -0500 Subject: [PATCH 31/37] Initial version of !p with f-strings. --- Include/ceval.h | 7 ++++--- Include/object.h | 1 + Lib/test/test_fstring.py | 4 ++-- Lib/test/test_print.py | 3 +++ Lib/test/test_tstring.py | 2 +- Objects/object.c | 20 ++++++++++++++++++++ Objects/stringlib/unicode_format.h | 4 +++- PC/python3dll.c | 1 + Parser/action_helpers.c | 4 ++-- Python/bytecodes.c | 2 +- Python/ceval.c | 5 +++-- Python/codegen.c | 2 ++ Python/generated_cases.c.h | 2 +- 13 files changed, 44 insertions(+), 13 deletions(-) diff --git a/Include/ceval.h b/Include/ceval.h index e9df8684996e23..af65feea546e4e 100644 --- a/Include/ceval.h +++ b/Include/ceval.h @@ -125,13 +125,14 @@ PyAPI_FUNC(void) PyEval_ReleaseThread(PyThreadState *tstate); } /* Masks and values used by FORMAT_VALUE opcode. */ -#define FVC_MASK 0x3 +#define FVC_MASK 0x7 #define FVC_NONE 0x0 #define FVC_STR 0x1 #define FVC_REPR 0x2 #define FVC_ASCII 0x3 -#define FVS_MASK 0x4 -#define FVS_HAVE_SPEC 0x4 +#define FVC_PRETTY 0x4 +#define FVS_MASK 0x8 +#define FVS_HAVE_SPEC 0x8 #ifndef Py_LIMITED_API # define Py_CPYTHON_CEVAL_H diff --git a/Include/object.h b/Include/object.h index ad452be8405671..f958974cc4861d 100644 --- a/Include/object.h +++ b/Include/object.h @@ -466,6 +466,7 @@ PyAPI_FUNC(void) PyType_Modified(PyTypeObject *); PyAPI_FUNC(PyObject *) PyObject_Repr(PyObject *); PyAPI_FUNC(PyObject *) PyObject_Str(PyObject *); PyAPI_FUNC(PyObject *) PyObject_ASCII(PyObject *); +PyAPI_FUNC(PyObject *) PyObject_Pretty(PyObject *); PyAPI_FUNC(PyObject *) PyObject_Bytes(PyObject *); PyAPI_FUNC(PyObject *) PyObject_RichCompare(PyObject *, PyObject *, int); PyAPI_FUNC(int) PyObject_RichCompareBool(PyObject *, PyObject *, int); diff --git a/Lib/test/test_fstring.py b/Lib/test/test_fstring.py index 05d0cbd2445c4c..983638aa880883 100644 --- a/Lib/test/test_fstring.py +++ b/Lib/test/test_fstring.py @@ -1369,7 +1369,7 @@ def test_conversions(self): for conv_identifier in 'g', 'A', 'G', 'ä', 'ɐ': self.assertAllRaise(SyntaxError, "f-string: invalid conversion character %r: " - "expected 's', 'r', or 'a'" % conv_identifier, + "expected 's', 'r', 'a', or 'p'" % conv_identifier, ["f'{3!" + conv_identifier + "}'"]) for conv_non_identifier in '3', '!': @@ -1385,7 +1385,7 @@ def test_conversions(self): self.assertAllRaise(SyntaxError, "f-string: invalid conversion character 'ss': " - "expected 's', 'r', or 'a'", + "expected 's', 'r', 'a', or 'p'", ["f'{3!ss}'", "f'{3!ss:}'", "f'{3!ss:s}'", diff --git a/Lib/test/test_print.py b/Lib/test/test_print.py index cd3139837d4dee..3973019cca148d 100644 --- a/Lib/test/test_print.py +++ b/Lib/test/test_print.py @@ -239,6 +239,9 @@ def test_bad_pprinter(self): with self.assertRaises(AttributeError): print('one', PPrintable(), 2, file=self.file, pretty=object()) + def test_fstring(self): + self.assertEqual(f'{PPrintable()!p}', "PPrintable('I feel pretty')") + if __name__ == "__main__": unittest.main() diff --git a/Lib/test/test_tstring.py b/Lib/test/test_tstring.py index 74653c77c55de1..93a92578333bd6 100644 --- a/Lib/test/test_tstring.py +++ b/Lib/test/test_tstring.py @@ -216,7 +216,7 @@ def test_syntax_errors(self): ("t'{x!}'", "t-string: missing conversion character"), ("t'{x=!}'", "t-string: missing conversion character"), ("t'{x!z}'", "t-string: invalid conversion character 'z': " - "expected 's', 'r', or 'a'"), + "expected 's', 'r', 'a', or 'p'"), ("t'{lambda:1}'", "t-string: lambda expressions are not allowed " "without parentheses"), ("t'{x:{;}}'", "t-string: expecting a valid expression after '{'"), diff --git a/Objects/object.c b/Objects/object.c index ab73d2eb1c9c1f..ed8bfe18844b9f 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -862,6 +862,26 @@ PyObject_ASCII(PyObject *v) return res; } +PyObject * +PyObject_Pretty(PyObject *v) +{ + /* Call `pprint.pformat` */ + PyObject *printer = PyImport_ImportModuleAttrString("pprint", "pformat"); + if (!printer) { + return NULL; + } + + PyObject *prettified = PyObject_CallOneArg(printer, v); + + if (!prettified) { + Py_DECREF(printer); + return NULL; + } + + Py_DECREF(printer); + return prettified; +} + PyObject * PyObject_Bytes(PyObject *v) { diff --git a/Objects/stringlib/unicode_format.h b/Objects/stringlib/unicode_format.h index ff32db65b11a0b..5a234b79838bfc 100644 --- a/Objects/stringlib/unicode_format.h +++ b/Objects/stringlib/unicode_format.h @@ -760,7 +760,7 @@ MarkupIterator_next(MarkupIterator *self, SubString *literal, } -/* do the !r or !s conversion on obj */ +/* do the !r, !s, !a, or !p conversion on obj */ static PyObject * do_conversion(PyObject *obj, Py_UCS4 conversion) { @@ -773,6 +773,8 @@ do_conversion(PyObject *obj, Py_UCS4 conversion) return PyObject_Str(obj); case 'a': return PyObject_ASCII(obj); + case 'p': + return PyObject_Pretty(obj); default: if (conversion > 32 && conversion < 127) { /* It's the ASCII subrange; casting to char is safe diff --git a/PC/python3dll.c b/PC/python3dll.c index b23bc2b8f4382f..4f29648c708418 100755 --- a/PC/python3dll.c +++ b/PC/python3dll.c @@ -482,6 +482,7 @@ EXPORT_FUNC(PyNumber_TrueDivide) EXPORT_FUNC(PyNumber_Xor) EXPORT_FUNC(PyObject_AsCharBuffer) EXPORT_FUNC(PyObject_ASCII) +EXPORT_FUNC(PyObject_Pretty) EXPORT_FUNC(PyObject_AsFileDescriptor) EXPORT_FUNC(PyObject_AsReadBuffer) EXPORT_FUNC(PyObject_AsWriteBuffer) diff --git a/Parser/action_helpers.c b/Parser/action_helpers.c index 1f5b6220ba1baa..865ac2ebedec7e 100644 --- a/Parser/action_helpers.c +++ b/Parser/action_helpers.c @@ -1001,9 +1001,9 @@ _PyPegen_check_fstring_conversion(Parser *p, Token* conv_token, expr_ty conv) Py_UCS4 first = PyUnicode_READ_CHAR(conv->v.Name.id, 0); if (PyUnicode_GET_LENGTH(conv->v.Name.id) > 1 || - !(first == 's' || first == 'r' || first == 'a')) { + !(first == 's' || first == 'r' || first == 'a' || first == 'p')) { RAISE_SYNTAX_ERROR_KNOWN_LOCATION(conv, - "%c-string: invalid conversion character %R: expected 's', 'r', or 'a'", + "%c-string: invalid conversion character %R: expected 's', 'r', 'a', or 'p'", TOK_GET_STRING_PREFIX(p->tok), conv->v.Name.id); return NULL; diff --git a/Python/bytecodes.c b/Python/bytecodes.c index b461f9b5bea8a6..99f13a6c69a0fd 100644 --- a/Python/bytecodes.c +++ b/Python/bytecodes.c @@ -5135,7 +5135,7 @@ dummy_func( inst(CONVERT_VALUE, (value -- result)) { conversion_func conv_fn; - assert(oparg >= FVC_STR && oparg <= FVC_ASCII); + assert(oparg >= FVC_STR && oparg <= FVC_PRETTY); conv_fn = _PyEval_ConversionFuncs[oparg]; PyObject *result_o = conv_fn(PyStackRef_AsPyObjectBorrow(value)); PyStackRef_CLOSE(value); diff --git a/Python/ceval.c b/Python/ceval.c index ab2eef560370f5..0427c73e1610ee 100644 --- a/Python/ceval.c +++ b/Python/ceval.c @@ -350,10 +350,11 @@ const binaryfunc _PyEval_BinaryOps[] = { [NB_SUBSCR] = PyObject_GetItem, }; -const conversion_func _PyEval_ConversionFuncs[4] = { +const conversion_func _PyEval_ConversionFuncs[5] = { [FVC_STR] = PyObject_Str, [FVC_REPR] = PyObject_Repr, - [FVC_ASCII] = PyObject_ASCII + [FVC_ASCII] = PyObject_ASCII, + [FVC_PRETTY] = PyObject_Pretty }; const _Py_SpecialMethod _Py_SpecialMethods[] = { diff --git a/Python/codegen.c b/Python/codegen.c index 42fccb07d31dba..72873f230259f3 100644 --- a/Python/codegen.c +++ b/Python/codegen.c @@ -4243,6 +4243,7 @@ codegen_interpolation(compiler *c, expr_ty e) case 's': oparg |= FVC_STR << 2; break; case 'r': oparg |= FVC_REPR << 2; break; case 'a': oparg |= FVC_ASCII << 2; break; + case 'p': oparg |= FVC_PRETTY << 2; break; default: PyErr_Format(PyExc_SystemError, "Unrecognized conversion character %d", conversion); @@ -4270,6 +4271,7 @@ codegen_formatted_value(compiler *c, expr_ty e) case 's': oparg = FVC_STR; break; case 'r': oparg = FVC_REPR; break; case 'a': oparg = FVC_ASCII; break; + case 'p': oparg = FVC_PRETTY; break; default: PyErr_Format(PyExc_SystemError, "Unrecognized conversion character %d", conversion); diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h index 37fa6d679190dd..9f33d4e876b5f2 100644 --- a/Python/generated_cases.c.h +++ b/Python/generated_cases.c.h @@ -5265,7 +5265,7 @@ _PyStackRef result; value = stack_pointer[-1]; conversion_func conv_fn; - assert(oparg >= FVC_STR && oparg <= FVC_ASCII); + assert(oparg >= FVC_STR && oparg <= FVC_PRETTY); conv_fn = _PyEval_ConversionFuncs[oparg]; _PyFrame_SetStackPointer(frame, stack_pointer); PyObject *result_o = conv_fn(PyStackRef_AsPyObjectBorrow(value)); From cb7e20444de93d27f899100459f6c979d7f06a05 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 22 Feb 2026 19:04:05 -0800 Subject: [PATCH 32/37] Update Objects/object.c --- Objects/object.c | 1 + 1 file changed, 1 insertion(+) diff --git a/Objects/object.c b/Objects/object.c index ed8bfe18844b9f..c86e5a57675d1c 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -872,6 +872,7 @@ PyObject_Pretty(PyObject *v) } PyObject *prettified = PyObject_CallOneArg(printer, v); + Py_DECREF(printer); if (!prettified) { Py_DECREF(printer); From e1c45f2ab8fde7a30acbfa8535d88fa1bf25cd5a Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 22 Feb 2026 19:04:12 -0800 Subject: [PATCH 33/37] Update Objects/object.c --- Objects/object.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/object.c b/Objects/object.c index c86e5a57675d1c..719816c074faf0 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -875,7 +875,6 @@ PyObject_Pretty(PyObject *v) Py_DECREF(printer); if (!prettified) { - Py_DECREF(printer); return NULL; } From bbf612c2f17b0fbd2914a38af3403bce896d6706 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Sun, 22 Feb 2026 19:04:17 -0800 Subject: [PATCH 34/37] Update Objects/object.c --- Objects/object.c | 1 - 1 file changed, 1 deletion(-) diff --git a/Objects/object.c b/Objects/object.c index 719816c074faf0..71e0c56ce3ebb0 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -878,7 +878,6 @@ PyObject_Pretty(PyObject *v) return NULL; } - Py_DECREF(printer); return prettified; } From 90f5b795ad9e444ee744af815e82f9203e447014 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 23 Feb 2026 08:53:16 -0800 Subject: [PATCH 35/37] ruff can't handle the new !p specifier, so ignore that whole file for now A #noqa pragma won't work. --- .pre-commit-config.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1dcb50e31d9a68..6565525d7cd80e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,6 +14,8 @@ repos: name: Run Ruff (lint) on Lib/test/ args: [--exit-non-zero-on-fix] files: ^Lib/test/ + # TODO: remove this exclude once !p f-string support is merged to main + exclude: ^Lib/test/test_print\.py$ - id: ruff-check name: Run Ruff (lint) on Tools/build/ args: [--exit-non-zero-on-fix, --config=Tools/build/.ruff.toml] From 0d8320b160a835985363e3c1f5e8bc19b4befed7 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 23 Feb 2026 09:59:01 -0800 Subject: [PATCH 36/37] Add a blurb for PEP 813 implementation --- .../2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst new file mode 100644 index 00000000000000..69cf4c61a2bd39 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-23-09-58-49.gh-issue-145153.Khbemz.rst @@ -0,0 +1,2 @@ +`PEP 813 `_ (Pretty Print Protocol) +implementation. From b331439b8f8c22b5549c9c3d6dbdcb5f61cd0fd4 Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 23 Feb 2026 16:27:29 -0800 Subject: [PATCH 37/37] False-y names are treated as positional arguments. Also add some tests for some corner cases. --- Lib/pprint.py | 13 ++++++++++++- Lib/test/test_pprint.py | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/Lib/pprint.py b/Lib/pprint.py index ee99a7454e07ec..616c65ef097723 100644 --- a/Lib/pprint.py +++ b/Lib/pprint.py @@ -643,11 +643,22 @@ def _format_pprint(self, object, method, context, maxlevels, level): formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1) parts.append(f"{name}={formatted}") readable = readable and is_readable - case (name, value): + case (str() as name, value) if name: # Keyword argument. Always show. formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1) parts.append(f"{name}={formatted}") readable = readable and is_readable + case (name, value) if not name: + # 2-tuple with a false-y name: treat as positional. + formatted, is_readable, _ = self.format(value, context, maxlevels, level + 1) + parts.append(formatted) + readable = readable and is_readable + case (name, value): + # Truthy non-string name is an error. + raise ValueError( + f"__pprint__ yielded a 2-tuple with " + f"non-string name: {name!r}" + ) case _: # Positional argument. formatted, is_readable, _ = self.format(item, context, maxlevels, level + 1) diff --git a/Lib/test/test_pprint.py b/Lib/test/test_pprint.py index ffd6e3df7d1a63..dc1707f9dea454 100644 --- a/Lib/test/test_pprint.py +++ b/Lib/test/test_pprint.py @@ -1602,6 +1602,46 @@ def __pprint__(self): self.assertTrue(pprint.isreadable(Readable())) self.assertFalse(pprint.isreadable(Unreadable())) + def test_pprint_protocol_falsey_names(self): + # Any 2-tuple form with a falsey name gets treated as a positional argument. + class IsFalse: + def __bool__(self): + return False + + # 2-tuple form with falsey names are treated as positional. + class PositionalTuples: + def __pprint__(self): + yield None, (1, 2) + yield False, (3, 4) + yield 0, (5, 6) + yield IsFalse(), (7, 8) + + stream = io.StringIO() + pprint.pprint(PositionalTuples(), stream=stream) + self.assertEqual( + stream.getvalue(), + 'PositionalTuples((1, 2), (3, 4), (5, 6), (7, 8))\n' + ) + + def test_pprint_protocol_truthy_nonstring_names(self): + # 2-tuple form with truthy, non-str name is an error. + class BrokenPrinter_1: + def __pprint__(self): + yield 7, 'hello' + + self.assertRaises(ValueError, pprint.pprint, BrokenPrinter_1()) + + # The name argument must be exactly a str. + class Strable: + def __str__(self): + yield 'strable' + + class BrokenPrinter_2: + def __pprint__(self): + yield Strable(), 'hello' + + self.assertRaises(ValueError, pprint.pprint, BrokenPrinter_2()) + class DottedPrettyPrinter(pprint.PrettyPrinter):