PEP 813 – The Pretty Print Protocol
- Author:
- Barry Warsaw <barry at python.org>, Eric V. Smith <eric at trueblade.com>
- Discussions-To:
- Discourse thread
- Status:
- Draft
- Type:
- Standards Track
- Created:
- 07-Nov-2025
- Python-Version:
- 3.16
- Post-History:
- 21-Feb-2026, 04-Mar-2026, 24-Oct-2026
Table of Contents
- Abstract
- Motivation
- Specification
- Rationale
- Examples
- Backwards Compatibility
- Security Implications
- How to Teach This
- Reference Implementation
- Rejected Ideas
- Return types other than plain tuples
- Section markers as
Layoutsubclasses - Reading tuples by their shape
- Requiring an order of sections
- Passing layout state to
__pprint__() - Plain inheritance of
__pprint__() - Only a
__pprint__()defined on the object’s own class - Rebuilding
PrettyPrinter - Keys on one line
- A
pformat()method on printers - Handling strings and integers natively
- A
Delegate(obj)wrapper - Format specs for padding and alignment
- Deferred Ideas
- Open Issues
- Acknowledgments
- Footnotes
- Change History
- Copyright
Abstract
This PEP describes the “pretty print protocol”, a collection of changes proposed to make pretty printing more
customizable and convenient. Classes describe the structure of their instances with a new __pprint__()
method, and a printer decides how to lay that structure out. The built-in and standard library types
implement __pprint__() too, and a new default printer lays out every object the same way. A new
!p conversion pretty prints a value in f-strings and str.format(), and an f-string can name the
printer to use. T-strings record !p and its printer for the code that processes the template.
Motivation
“Pretty printing” is a feature which provides capabilities to format object representations for
better readability. The core functionality is implemented by the standard library pprint
module. pprint includes a class and APIs which users can invoke to format and print more
readable representations of objects, versus the standard repr() built-in function. Important
use cases include pretty printing large dictionaries and other complicated objects for debugging
purposes.
Today, pprint can only lay out the types it knows about. It keeps a private dispatch table of 25 types,
keyed on each type’s __repr__() method, plus a special case for dataclasses. Every other object is
printed with repr() as a single unbreakable piece of text, however much nested data it contains. A
third-party class has no way to tell pprint about its structure, so its instances never wrap.
This PEP provides more customization and user convenience. It is also inspired by the Rich library’s pretty printing protocol.
Specification
There are several parts to this proposal.
__pprint__() methods
Classes can implement a new dunder method, __pprint__(), which describes the pretty printed form of their
instances. This augments __repr__() which, prior to this proposal, was the only method used to generate a
custom representation of the object.
__pprint__() is optional. If it is missing, printers fall back to the object’s __repr__().
If it is defined on a class, __pprint__() takes a single argument, the object to be pretty
printed (typically self), and returns an iterable, usually by being a generator. The items it yields
form a description stream, which a printer interprets.
For annotations, pprint defines pprint.DescriptionType, a type alias for the result of a
__pprint__() method, defined as type DescriptionType = Iterable[object]:
def __pprint__(self) -> pprint.DescriptionType:
...
Since any object can be a value in the stream, the alias does not constrain the items. It documents what the method returns, and gives the documentation one place to describe the stream.
An inherited __pprint__() is used only if the class that provides it comes no later in the method
resolution order than the class that provides __repr__(). So a subclass that overrides __repr__()
is printed with its own repr(), as it is by pprint today, unless it also provides __pprint__().
A subclass can opt back in to its base class’s method with one line:
class D(dict):
def __repr__(self):
...
__pprint__ = dict.__pprint__
A mixin that provides __pprint__() works as expected, because in class C(Mixin, Base) the mixin
comes before Base in the method resolution order. See Inherited __pprint__() methods for the reasons behind
this rule.
The standard vocabulary
The pprint module defines these public names for use in a description stream.
pprint.VALUES,pprint.KWARGSandpprint.ITEMS- Section markers. Each marker sets how the items that follow it are read, until the next marker. The
stream starts in the
VALUESsection.Section Each item is Prints as Mirrors VALUESAny value, tuples included valuePositional arguments KWARGS(name, value)or(name, value, default)name=valueKeyword arguments ITEMS(key, value)key: valuedict.items()Under
KWARGS,nameis astrand is printed as is. A(name, value, default)item is omitted whenvalue == defaultis true. If evaluatingbool(value == default)raises anException, the item is shown with an error placeholder in place of its value, such asarr=<error comparing to default: ValueError>. UnderITEMS,keyis pretty printed like any other value, so a string key gets its quotes and a container key can wrap. pprint.DELEGATE- If
DELEGATEis the first item in the stream, the next item is printed in place of the object.collections.UserDictuses this to print as its underlyingdict.
The section markers and DELEGATE are PEP 661 sentinel objects.
pprint.Layout- Describes an object’s chrome: the name and brackets around its items, the separator between them,
and a few special cases. It can also carry hints for particular printers. A
__pprint__()method sets the chrome of the object being printed by yielding aLayoutinstance as the first item in the stream. Without one, the printer uses the defaults below. The constructor takes these keyword-only arguments, all optional:name: the name used in the default brackets. Without it,type(obj).__name__is used.openandclose: the text before and after the items. Without them, the object is printed as its name followed by the items in parentheses, or in({...})when every item is a key/value item, as inCounter({'a': 1}).sep: the separator between items,','by default. When the items fit on one line, a space follows each separator.empty: the text printed when there are no items,openfollowed bycloseby default. For example,setuses'set()'.single_close: the text used in place ofclosewhen there is exactly one item, as in the',)'of a one-element tuple.recursion: the text printed for a reference cycle back to this object,'...'by default. For example,listuses'[...]', matchingrepr().sortable: whether a printer may sort the items, because their order is not significant.Falseby default. Nested objects describes when the default printer sorts them.hug: whether a printer keeps the other items on the opening and closing lines when exactly one item is a container that does not fit, and lays out only that item on lines of its own.Falseby default. It affectsExpand.FITandExpand.ALWAYS. Built-in and standard library types shows an example.hints: a mapping of hints for particular printers, empty by default. By convention, each key names the printer that reads it, as in'html.role'. A printer ignores the keys it does not understand, and the default printer ignores them all.pprint.decompose()passes them through unchanged.
pprint.Keywordandpprint.Entry- Small immutable classes,
Keyword(name, value, default)withdefaultoptional, andEntry(key, value).pprint.decompose()returns them for keyword and key/value items. A method may also yield them directly, in any section. They are not tuples, so they are never mistaken for tuple values. pprint.BreakableStringandpprint.BreakableBytes- Small immutable classes,
BreakableString(value)andBreakableBytes(value), holding astrorbytesvalue that a printer may split across lines. The default printer prints one as therepr()of its value, split into adjacent literals to fit the width, asPrettyPrintersplits long strings and bytes today.strandbytesdescribe themselves with them:class str: def __pprint__(self): yield DELEGATE yield BreakableString(self) class bytes: def __pprint__(self): yield DELEGATE yield BreakableBytes(self)
Any other class that contains
strorbytesvalues can yield these markers if it wants a wrappable representation.pprint.decompose()treats them as leaves, and a printer recognizes them by their type. pprint.FormattableNumber- This class takes a
value, similar toBreakableStringandBreakableBytes. The default printer formats the value with itsnumber_formatoption, described in Printers. Theintandfloattypes describe themselves with this marker, andpprint.decompose()treats it like a leaf.
The default printer reads the stream permissively:
- Section markers may appear in any order, any number of times. Items are printed in the order they are yielded.
- An item that does not fit its section is read as a value. For example, a 4-tuple under
KWARGSis printed as a tuple. - Any object that is not part of the vocabulary is a value. So a marker that only a user-supplied custom
printer understands is printed as a value, usually with its
repr(). - A
Layoutthat is not the first item, and aDELEGATEthat is not the first item, are values. IfDELEGATEis the only item, the object is printed withrepr(). Items after the delegated value are ignored. - The printer does not check that its output is valid Python. For example,
KWARGSitems insidelist-like brackets print as[a=1]. - Exceptions raised by
__pprint__()itself, or while iterating over its result, propagate, as exceptions raised by__repr__()do.
Leaves
A leaf is a value that the printer does not decompose any further, because its type has no usable
__pprint__(). pprint.decompose() returns (pprint.LEAF, None) for it. The default printer
prints most leaves with repr(), as one unbreakable piece of text. The exceptions are the markers
BreakableString, BreakableBytes and FormattableNumber, which are leaves that a printer
recognizes by their type. The default printer splits a breakable value across lines to fit the width, and
formats a number by passing its number_format option to format(), or with repr() when that
option is not set.
str, bytes, int and float are not leaves themselves. They implement __pprint__() by
delegating to one of the above markers. So a subclass that overrides __repr__(), such as bool, is
printed with its own repr(), by the same rule as any other inherited __pprint__().
An ordinary object does not have to declare that it is a leaf. The printer decomposes each value it is given, and recursion stops on its own at leaves.
pprint.decompose()
pprint.decompose(obj) is a new module-level function. It finds the usable __pprint__() method of
type(obj), runs it, and normalizes one level of the stream according to the rules above. It returns a
2-tuple (layout, items):
(pprint.LEAF, None)ifobjis a leaf.(pprint.DELEGATE, value)if the object delegates tovalue.- Otherwise, a
Layoutwith every attribute filled in, and a list of items, each a plain value, aKeyword, or anEntry. Keyword items equal to their defaults have been removed, and section markers do not appear.
So the first element always says what kind of result it is, and a caller can match on it. A leaf’s
second element is None rather than an empty sequence, so that a leaf is never mistaken for an empty
container, which returns its Layout and an empty list.
pprint.LEAF is a new PEP 661 sentinel. It is used only in the result of decompose(), and it is
not part of the stream vocabulary. If a __pprint__() method yields it, it is printed as a value, like
any other object outside the vocabulary. An object is a leaf when its type has no usable
__pprint__() method, which includes the marker classes BreakableString, BreakableBytes and
FormattableNumber.
decompose() never recurses into the items, and it does not depend on any printer setting. The default
printer calls it on each object it prints. Other printers can call it to get the same reading of a stream.
A printer with its own vocabulary can call it too, and look for its own markers among the plain values,
since decompose() passes anything it does not understand through as a value.
Printers
A printer is any callable that takes the object to print as its only argument and returns an iterable of
str, one for each line of output, without line endings. A plain function can be a printer. Code that
needs the output as a single string joins the lines with '\n', as !p does. Nothing checks a line
for embedded newlines, so a line that contains one is joined as is.
The default printer is an instance of a new class, pprint.Printer. Its constructor takes the options of
pprint.PrettyPrinter, except stream and underscore_numbers, with the same meanings, except
for expand as described below. Its instances are printers.
In place of PrettyPrinter’s underscore_numbers, Printer takes number_format, a format spec
that the printer applies to each FormattableNumber with format(). So number_format='_'
groups digits as underscore_numbers=True does, and number_format=',' groups them with commas, as
in 1,234,567.125. If format() raises an exception for a value, the value is printed with
repr(). The default, None, prints every number with repr(). Any spec that the
format specification mini-language accepts can be used, so the option gains whatever
the mini-language adds later.
Three of its defaults differ from PrettyPrinter’s: width is 78, indent is 4, and expand is
Expand.FIT. So by default its output stays within the line length that PEP 8 recommends, and comes
close to code formatted by tools such as ruff and black. As with PrettyPrinter, sort_dicts
is True by default.
The expand option takes a member of a new enumeration, pprint.Expand, which says how a container
that does not fit on one line is laid out:
Expand.NEVER- The items follow the open bracket and are aligned under it, as
PrettyPrinterlays them out by default. The items are not indented in this layout, soindentis used only for a value that starts below a wrapped key, as described in Nested objects. Expand.FIT- Each item is on its own line, indented, with a trailing separator, and the close bracket is on a line of
its own. Each nested container is laid out this way only if it does not fit on one line itself. This
is what
PrettyPrinter’sexpand=Truemeans. This is the default. Expand.ALWAYS- The same layout as
FIT, except that once a container does not fit, every non-empty container inside it is laid out this way too, whether or not it fits.
In every mode, an object that fits on one line is printed on one line.
With compact=True, the positional items of a container that does not fit are packed onto as few lines
as possible, as PrettyPrinter packs them. An item that does not fit on one line gets lines of its own.
With Expand.FIT and Expand.ALWAYS, the packed lines are indented like any other items. With
Expand.ALWAYS, a non-empty container is never packed, since it is always expanded. PrettyPrinter
does not allow compact with expand=True.
For the built-in and standard library types, the default printer’s output generally matches
PrettyPrinter’s with equivalent options, where Expand.NEVER is equivalent to expand=False with
indent=1, and Expand.FIT is equivalent to expand=True. It can also differ where
PrettyPrinter breaks a line that would fit, as it does for a container nested in a deque. The main
exceptions are described in Nested objects, along with these:
- Objects with a
__pprint__()method wrap to fit the width. - A reference cycle prints the
recursiontext of the object’sLayout, such as{...}, at every level. At the top level this replacesPrettyPrinter’s<Recursion on dict with id=...>. - With
Expand.NEVER, the items of thedictviews and thecollections.abc.MappingViewtypes are aligned under the open bracket.PrettyPrinterdoes not align them. - With
Expand.FITandExpand.ALWAYS, the continuation lines of a long string that is adictvalue or a keyword value are aligned under its first piece.PrettyPrinterputs them at the indentation of the item instead. - With
Expand.NEVER,indentdoes not pad the items. Withexpand=False,PrettyPrinteruses it as padding after each open bracket, as in{ 'key': [ 'value',. - An
collections.OrderedDictis printed in the form of itsrepr(),OrderedDict({...}), even when it does not fit on one line.PrettyPrinterswitches to a list of(key, value)tuples, the form ofrepr()before Python 3.12. - With
compact=True, a packed line can end at the last column of the width.PrettyPrinterreserves room for a separator after the last item, so it breaks such a line one item earlier.
PrettyPrinter, and the module functions such as pprint.pprint() and pprint.pformat() that
use it, are unchanged.
Nested objects
The default printer decomposes every object it prints, including each value, each keyword value, and each
key and value of a key/value item. So nested objects wrap inside their containers at any depth, and a
__pprint__() method never handles the layout of its items. These rules apply to nested objects:
- Keys
- A key is laid out like any other object, so a key with a
__pprint__()method, such as afrozensetorfrozendict, can wrap.PrettyPrinteralways prints a key on one line, however long it is. - Values after a wrapped key
- A value normally starts on the same line as its key, after the
': '. If the key wraps, the value starts on a new line instead, indented one level from the start of the key. Placed after the key’s last line, the value would start far to the right, and its own items would be pushed further. WithExpand.FITandExpand.ALWAYS, the last line of a wrapped key is just its closing bracket, so the value follows the key as usual. - Sorting
- When
sort_dictsisTrue, the items of an object whoseLayoutissortableare sorted, key/value items by key and positional items by value, in their natural order, asPrettyPrintersorts them. If these keys and values can’t be compared with each other, or any of them is a set, they are sorted by their pretty printed form on one line instead. A set’s comparison operators test inclusion rather than order, so sorting sets in their natural order gives an arbitrary result. The one-line form does not depend on hash order the wayrepr()does, since a set’s own items are sorted too.PrettyPrinterinstead orders each pair of items that can’t be compared by their type names andid()s, so its order can change from run to run. Keyword items are sorted by name, after all the other items, so the output stays in the order of a call. - Depth
- A key and its value are both one level deeper than their container, so
depthelides deeply nested keys as it does values. The limit applies however an object is laid out.PrettyPrinterapplies it only to objects whose elided form fits on the rest of the line.
Built-in and standard library types
These types implement __pprint__(), allowing the default printer to format the same types that
pprint.PrettyPrinter supports today: str, bytes, bytearray, int, float,
dict, frozendict, list, tuple, set, frozenset, the dict views,
types.MappingProxyType, types.SimpleNamespace, collections.OrderedDict,
collections.defaultdict, collections.Counter, collections.ChainMap,
collections.deque, collections.UserDict, collections.UserList,
collections.UserString, collections.abc.MappingView, and
string.templatelib.Template and string.templatelib.Interpolation.
For example, dict and tuple behave as if they were written:
class dict:
def __pprint__(self):
yield Layout(open='{', close='}', empty='{}', recursion='{...}', sortable=True)
yield ITEMS
yield from self.items()
class tuple:
def __pprint__(self):
yield Layout(open='(', close=')', empty='()', single_close=',)', recursion='(...)')
yield from self
Counter and OrderedDict are not sortable, since their order is meaningful.
The __pprint__() methods of defaultdict, mappingproxy, the MappingView types and deque
yield a Layout with hug=True, so with Expand.FIT they keep the layout that PrettyPrinter
gives them with expand=True:
defaultdict(<class 'list'>, {
'precision': ['E', 'A', 'D', 'G'],
'stingray': ['B', 'E', 'A', 'D', 'G'],
})
Without hug, the factory would be printed on a line of its own. Similarly, a deque with a
maxlen keeps ], maxlen=10) on its closing line.
dataclasses and named tuples
When the dataclass() decorator generates a __repr__() method, it also generates a
__pprint__() method, which describes the same fields that __repr__() shows. It uses the class’s
__qualname__, as __repr__() does. If the class body defines its own __repr__(), no
__pprint__() is generated, so the method resolution order rule above keeps the hand-written repr() in
use. For Item(count: int, tags: list), the generated method is equivalent to:
def __pprint__(self):
yield pprint.Layout(name=self.__class__.__qualname__)
yield pprint.KWARGS
yield 'count', self.count
yield 'tags', self.tags
Classes made by collections.namedtuple() get a generated __pprint__() in the same way, so a long
named tuple wraps.
Additions to f-strings and str.format()
In addition to the existing !s, !r, and !a conversion specifiers, a new !p conversion
specifier will be added. The effect of this specifier with an expression value will be to call the
default printer (importing the pprint module as needed), passing value as the only argument, and
to join the lines it returns with '\n'. There is no trailing newline.
In f-strings, the !p conversion accepts an optional printer, given as a format spec consisting of a
single replacement field: f'{obj!p:{printer}}'. The printer expression is evaluated after obj,
and its value is used directly, without being converted to a string. If the value is None, the default
printer is used, as if no printer were given. The result of the f-string field is the lines of
printer(obj), joined with '\n'. TypeError is raised if printer(obj)
returns a str, which would otherwise be read as one line per character, or if any line is not a
str. The !p conversion is also compatible with the obj= form, e.g. f'{obj=!p:{printer}}'.
The replacement field in the format spec must have no conversion or format spec of its own. Any other format
spec, such as f'{obj!p:>40}', is a SyntaxError, since its shape is known at compile time. An
empty spec, as in f'{obj!p:}', is the same as no spec. There is no need for padding or alignment specs,
since a printer can provide them.
In str.format(), a format spec after !p is applied to the pretty printed string, as it is after
!s, !r and !a. So '{!p:>40}'.format(obj) pads the result. A printer cannot be passed this
way, since str.format() converts a nested replacement field to a string before it applies the spec.
See Deferred Ideas.
T-strings
The !p conversion is also allowed in t-strings. As with the other conversions, it is not applied when
the template is created. The Interpolation records it, and the code that
processes the template decides what to do with it:
Interpolation.conversionis'p'.- A printer given in the format spec, as in
t'{obj!p:{printer}}', is evaluated when the template is created, and kept without being converted to a string, in a new attribute,Interpolation.printer. It isNonewhen no printer is given.Interpolation.format_specis the empty string. - The format spec follows the same rules as in f-strings. It must be empty or a single plain replacement
field, and anything else is a
SyntaxError. - The
Interpolationconstructor takes a new optionalprinterargument, which is accepted only with the'p'conversion.printeris added to__match_args__, and appears in therepr()when it is set. string.templatelib.convert()handles'p', and takes a new optionalprinterargument.convert(obj, 'p', printer)returns whatf'{obj!p:{printer}}'would, andconvert(obj, 'p')uses the default printer. In both, a printer ofNonemeans the default printer.
For example:
>>> from string.templatelib import convert
>>> basses = ['precision', 'jazz', 'stingray']
>>> printer = pprint.Printer(width=20)
>>> interpolation = t'{basses!p:{printer}}'.interpolations[0]
>>> interpolation.conversion, interpolation.printer is printer
('p', True)
>>> print(convert(interpolation.value, interpolation.conversion, interpolation.printer))
[
'precision',
'jazz',
'stingray',
]
Additions to the C-API
To support !p, a new function, PyObject_Pretty(), is added to the
Limited C API.
This function takes two arguments: a PyObject * for the object to pretty
print, and an optional PyObject * for the printer (which may be NULL).
When the printer is NULL or Py_None, this function calls the default
printer with the object as its argument. Otherwise, it calls the given printer
with the object as its argument. Either way, it joins the lines
that the printer returns with '\n' and returns the resulting str,
raising TypeError in the same cases as !p.
Returning a str mirrors PyObject_Str(), PyObject_Repr() and
PyObject_ASCII(), the functions behind !s, !r and !a. Both
f-strings and str.format() need the joined string, so the join and its
checks live in one place. C code that wants the lines calls the printer
directly.
Rationale
Pretty printing is very useful for displaying complex data structures, like dictionaries read from JSON content. By providing a way for classes to describe how their instances participate in pretty printing, users have more options for visually improving the display of their complex data, especially for debugging.
By adding a !p conversion specifier to f-strings and str.format(), debugging with user-friendly
display is made even more convenient. Since no extra imports are required, users can easily just piggyback
on well-worn “print debugging” patterns, at least for the most common use cases.
These extensions work both independently and complementarily, to provide powerful new use cases.
Description, not layout
A __pprint__() method describes what an object contains: its positional values, its keyword
arguments, or its key/value items, along with the brackets and name that surround them. It does not decide
where the line breaks go. The printer makes every layout decision, so width, indentation, depth and
sorting work the same way for every object, and a nested object wraps correctly inside its container
without knowing anything about the container. This also means a __pprint__() method never needs the
printer’s state passed to it.
Because the built-in containers describe themselves with the same protocol, the default printer has one layout algorithm for everything it prints. The reference implementation uses a Wadler-style document algebra with Oppen-style bounded lookahead, but the protocol does not depend on that choice.
A new default printer
This PEP adds a new default printer and leaves pprint.PrettyPrinter unchanged. The new layout
algorithm is different enough from PrettyPrinter’s that one class cannot keep its existing output and
overridable hooks working while also providing the new behavior.
PrettyPrinter also chooses a layout by looking each object’s type up in a hard-coded dispatch table. The
new printer has no table of types. Every object, including the built-in containers, describes itself through
__pprint__(), so a user-defined class takes part in exactly the same way that list and dict do.
Existing code that uses PrettyPrinter keeps its exact output, and code that wants the protocol opts in by
using the new printer.
If this PEP is accepted, PrettyPrinter will be soft deprecated, as PEP 387 describes. We plan no
removal, and it may continue to receive bug fixes, but we expect that no new features will be added to it.
New features will go to the new printer instead.
Sections, not guessing
The protocol is based on Rich’s, which reads each yielded tuple by its shape: a
2-tuple is a keyword argument and a 3-tuple is a keyword argument with a default. This PEP replaces that
guessing with explicit section markers, pprint.VALUES, pprint.KWARGS and pprint.ITEMS, which
say how the plain tuples that follow them are read. A positional tuple value needs no special spelling, and
a positional ('a', 1, 2) can no longer print silently as a=1. Items remain plain tuples, so a method
still needs no classes.
A stream that printers interpret
A __pprint__() method yields a stream of items that a printer interprets. This PEP defines a standard
vocabulary for the default printer. A user can also write a printer with a custom vocabulary, and give
their classes __pprint__() methods that yield it, while the default printer can still print the same
objects.
That is why the default printer is deliberately permissive. It does not enforce an order of markers, and it
prints an item it does not recognize as a value, usually with repr(), rather than raising an exception.
Pretty printing is mostly used for debugging, where output that shows a description is more helpful than an
exception about its shape.
Printers return lines
A printer returns the lines of its output rather than one string. Code that works line by line, such as
code that indents or prefixes each line for a log record or a doctest, then does not have to split the
output again. A printer that produces its lines lazily lets a caller print them immediately, preserving the
ability to stop at any point. Joining the lines is left to the code that needs a single string, such as
!p.
Inherited __pprint__() methods
An inherited __pprint__() is used only if its class comes no later in the method resolution order than
the class that provides __repr__().
This matches pprint today, which looks up its layout function by type(obj).__repr__. So
class D(dict): pass is pretty printed as a dict, but a dict subclass that overrides
__repr__() is printed with its own repr(). Once the built-in containers implement __pprint__(),
plain inheritance would change that for every such subclass.
More importantly, a description has to agree with repr(). A subclass that changes its repr() may
add fields, change its name or leave items out, and the __pprint__() method it inherits knows nothing
about that. The result would be misleading in a tool used for debugging. Worse, it could print what
repr() hides, such as the values of secret keys. See Security Implications.
Opting back in takes one line, as shown in the specification, and a mixin that provides __pprint__()
needs none. The same rule keeps a hand-written __repr__() in use for a class decorated with
dataclass().
Examples
A custom __pprint__() method can be used to customize the representation of the object, such as with this
class:
import pprint
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 pprint.KWARGS
yield 'pickups', self._pickups
yield 'active', self._active, False
Now let’s create a couple of instances and pretty print them:
>>> precision = Bass(4, 'split coil P', active=False)
>>> stingray = Bass(5, 'humbucker', active=True)
>>> print(f'{precision!p}')
Bass(4, pickups='split coil P')
>>> print(f'{stingray=!p}')
stingray=Bass(5, pickups='humbucker', active=True)
>>> print('{!p}'.format(precision))
Bass(4, pickups='split coil P')
For more complex objects, !p can help make debugging output more readable:
>>> 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(f'{os.pathconf_names = !p}')
os.pathconf_names = {
'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,
}
In f-strings, the !p conversion specifier also accepts a printer, such as a pprint.Printer with its
own options:
>>> narrow = pprint.Printer(width=30)
>>> print(f'{stingray!p:{narrow}}')
Bass(
5,
pickups='humbucker',
active=True,
)
>>> hanging = pprint.Printer(width=30, expand=pprint.Expand.NEVER)
>>> print(f'{stingray!p:{hanging}}')
Bass(5,
pickups='humbucker',
active=True)
A printer can also be a plain function that takes the object and returns its lines:
>>> def slappa(da: Bass) -> Iterator[str]:
... yield 'All about that bass'
>>> print(f'{precision=!p:{slappa}}')
precision=All about that bass
Since tuples are read according to the current section, a positional tuple value needs nothing special:
>>> class Things:
... def __pprint__(self):
... yield (1, 2)
... yield (3, 4)
... yield pprint.KWARGS
... yield 'arg', (5, 6)
...
>>> print(f'{Things()!p}')
Things((1, 2), (3, 4), arg=(5, 6))
Objects with a __pprint__() method wrap, however deeply they are nested:
class Node:
def __init__(self, name, children=None):
self.name = name
self.children = [] if children is None else children
def __pprint__(self):
yield self.name
yield pprint.KWARGS
yield 'children', self.children, []
>>> tree = Node('root', [Node('left', [Node('leaf')]), Node('right')])
>>> print(f'{tree!p:{pprint.Printer(width=60)}}')
Node(
'root',
children=[
Node('left', children=[Node('leaf')]),
Node('right'),
],
)
A Layout changes the brackets. This class prints in the angular form that Rich supports:
class Note:
def __init__(self, pitch, beats=1):
self.pitch = pitch
self.beats = beats
def __pprint__(self):
yield pprint.Layout(open='<Note ', close='>', sep='')
yield pprint.KWARGS
yield 'pitch', self.pitch
yield 'beats', self.beats, 1
>>> print(f"{Note('E', beats=2)!p}")
<Note pitch='E' beats=2>
>>> print(f"{Note('A')!p}")
<Note pitch='A'>
A hint asks a particular printer for a different presentation, without changing what other printers print.
This class asks an HTML printer to show it as a table, with <th> cells for the headers and a <tr>
for each row:
@dataclass
class Table:
headers: list[str]
rows: list[list[object]]
def __pprint__(self):
yield pprint.Layout(hints={'html.role': 'table'})
yield pprint.KWARGS
yield 'headers', self.headers
yield 'rows', self.rows
The default printer ignores the hint, so its output still agrees with repr():
>>> print(f"{Table(['Bass', 'Strings'], [['Precision', 4], ['Stingray', 5]])!p}")
Table(headers=['Bass', 'Strings'], rows=[['Precision', 4], ['Stingray', 5]])
The class writes its own __pprint__(), since the method that dataclasses generates has no way to
add a hint.
DELEGATE prints another object in place of this one. collections.UserDict is equivalent to:
class UserDict:
def __pprint__(self):
yield pprint.DELEGATE
yield self.data
A library that defines its own printer can also mark values for it. A wrapper class that delegates to the value it wraps degrades gracefully when it reaches a printer that doesn’t know about it:
class Emphasis:
def __init__(self, value):
self.value = value
def __pprint__(self):
# Printers that don't understand emphasis show the plain value.
yield pprint.DELEGATE
yield self.value
>>> notes = [Note('E'), Emphasis(Note('A')), Note('D')]
>>> print(f'{notes!p}')
[<Note pitch='E'>, <Note pitch='A'>, <Note pitch='D'>]
The default printer prints the wrapped value in place of the wrapper. A printer that understands
Emphasis recognizes it among the list’s items returned by pprint.decompose(), and shows the wrapped
value emphasized. A marker yielded in the list’s stream instead would appear in the default printer’s
output as a value. Unlike a Layout hint, which describes an object’s own presentation, the wrapper
marks a value inside another object.
Backwards Compatibility
This PEP adds new APIs and leaves pprint.PrettyPrinter and the existing pprint functions
unchanged, so existing code is not affected.
Other pretty printing protocols
Several third-party libraries have their own pretty printing protocols. This PEP’s protocol is not
compatible with them out of the box, but an object that supports one of them can add a __pprint__()
method that adapts to this PEP’s protocol, usually in a few lines. A library can also teach its own printer
to read __pprint__() descriptions by calling pprint.decompose(), which saves it from reimplementing
the rules of the stream.
- Rich
- Rich’s
__rich_repr__()method reads each yielded item by its shape: a plain value is positional, and a tuple starting with a name is a keyword argument, optionally with a default. Since this PEP reads tuples by section, a__rich_repr__()method cannot be used unchanged as a__pprint__()method, but it can be adapted:class Bird: def __rich_repr__(self): yield self.name yield 'eggs', self.eggs yield 'fly', self.fly, True def __pprint__(self): # Each Rich item says how it is meant, so switch sections to match. for item in self.__rich_repr__(): match item: case (str(), _) | (str(), _, _): yield pprint.KWARGS case _: yield pprint.VALUES yield item
Rich could provide such an adapter as a mixin or a decorator. Rich’s angular form,
<Name a=1>, can be expressed with aLayout, as in theNoteexample above. - IPython
- IPython’s
_repr_pretty_(self, p, cycle)method drives IPython’s printer directly, rather than describing the object. An object with a_repr_pretty_()method can add a__pprint__()method for other printers, and IPython’s printer could print objects that have only a__pprint__()method by callingpprint.decompose(). - Code generators
- Libraries that generate
__repr__()methods, such as attrs, can generate__pprint__()methods the same waydataclassesdoes.
Security Implications
A class may write a repr() that hides information, such as the values of secret keys in a mapping. If
the class inherits __pprint__() from a base class that yields every item, a printer would show what the
repr() hides. The rule for inherited __pprint__() methods prevents this, since
a class that overrides __repr__() is printed with that repr() unless it also provides or opts in to a
__pprint__() method. A class that does provide one is responsible for keeping it consistent with its
repr().
There are no other known security implications for this proposal.
How to Teach This
Documentation and examples are added to the pprint module, f-strings, t-strings, and
str.format().
Beginners don’t need to be taught these new features until they want prettier representations of
their objects.
Most classes need only plain values, pprint.KWARGS, and (name, value) or (name, value, default)
tuples, as in the Bass example. Layout, ITEMS and DELEGATE can be taught when a class wants
different brackets, holds key/value data, or wraps another object. Authors of printers are pointed to
pprint.decompose().
Reference Implementation
A reference implementation is in progress on a PEP author branch of the CPython main branch, tracked by python/cpython#145153.
Rejected Ideas
Return types other than plain tuples
We considered an alternative specification of the __pprint__() return
values, where either namedtuple()s, dataclasses, or a duck-typed instance
were used as the return types. Ultimately we rejected this because we don’t want to force folks to
define a new class just to return values from this function. Keyword and Entry may be yielded,
but plain tuples are always enough.
Section markers as Layout subclasses
We considered making KWARGS and ITEMS subclasses of Layout, so that one item could set both the
chrome and the first section. Sections can change in the middle of a stream, so the plain markers would
still be needed, and a section is not a kind of chrome. It would not help type annotations either, since any
object can be a value.
Reading tuples by their shape
The earlier version of this PEP followed Rich’s protocol and read a 2-tuple as a keyword argument and a
3-tuple as a keyword argument with a default, whatever their contents. A positional value that happened to
be a tuple could not be yielded as is, and had to be wrapped as (None, value). Forgetting the wrapper
raised no error, and the value was printed as something else. A positional ('a', 1, 2) came out as
a=1, and a positional ('a', 1, 1) disappeared, because its “value” equaled its “default”. See
Sections, not guessing.
Requiring an order of sections
We considered requiring VALUES before KWARGS, as in a call, with ITEMS standing alone, and raising
TypeError when these were given out of order. We rejected this in favor of a permissive printer, so
that other printers can interpret the stream according to their own rules, and so that debugging output
never fails because of the shape of a description stream.
Passing layout state to __pprint__()
We considered passing the width, indentation, current column and nesting level to __pprint__(), so that
an object could lay itself out. Every class would then need to reimplement layout, and nested objects would
need to cooperate with their containers. A description-only protocol keeps all layout in the printer.
Plain inheritance of __pprint__()
We considered letting __pprint__() be inherited like any other method. Once the built-in types
implement it, every subclass of them that overrides __repr__() would print differently under the new
printer than it does today, with output that may disagree with its repr(), or show what its repr()
hides. See Inherited __pprint__() methods.
Only a __pprint__() defined on the object’s own class
We considered using __pprint__() only when it is defined on type(obj) itself. Then
class D(dict): pass would no longer be pretty printed as a dict, which pprint does today, and a
mixin that provides __pprint__() would not work.
Rebuilding PrettyPrinter
We considered reimplementing pprint.PrettyPrinter on top of the protocol, replacing its dispatch
table. Its public format() method and its dispatch functions are hooks that subclasses can override,
and none of them fit a description-only design. Keeping them compatible while making the protocol work would
mean maintaining two layout engines in one class. See A new default printer.
Keys on one line
We considered keeping PrettyPrinter’s rule that a key is never wrapped, however long it is. A key can be
a container, such as a frozenset, so it can be as long as any value. Printed on one line, a long key
runs past the width, and its value, which starts after the key, is pushed even further to the right. See
Nested objects.
A pformat() method on printers
We considered requiring a printer to be an object with a pformat() method, so that PrettyPrinter
instances would be printers as is. Since the default printer is new, a plain callable is simpler, and a
function can be a printer.
Handling strings and integers natively
We considered having the printer recognize str, bytes and int and handle them itself, as
PrettyPrinter does. BreakableString, BreakableBytes and FormattableNumber make that
handling available to any class, and let these types follow the same rules as every other type, including
the rule for inherited methods. The cost is a __pprint__() call for each string and integer printed,
which are the most common values in large structures.
A Delegate(obj) wrapper
We considered a class wrapping the delegated object, yielded first in place of a Layout. A
DELEGATE sentinel followed by the value keeps Layout as the only class with a special meaning at
the start of a stream, and makes delegation one more sentinel alongside the section markers. Printers
and pprint.decompose() then read every stream the same way. It also avoids allocating a wrapper on
each call, a minor optimization.
Format specs for padding and alignment
Supporting specs like f'{obj!p:>40}' would mean choosing between a printer and a string format spec. A
printer can pad and align its own output, as in f'{obj!p:{MyPrinter(padding=40)}}'.
Deferred Ideas
- A printer format spec for
!pconversions instr.format(). - An
Expand.EXPLODEmember. It would lay out every non-empty container asFITdoes when a container does not fit, even when the whole object fits on one line, the same way ruff and black keep a collection exploded when it ends with a trailing comma (which a printer, working on objects rather than source, can’t see). - Printing the instance attributes of classes that use the default
object.__repr__(). Such a class prints as<Point object at 0x...>today. A__pprint__()method could instead yield its instance attributes underKWARGS, so that it prints asPoint(x=1, y=2), the waytypes.SimpleNamespacedoes.The first step would be an opt-in, such as a mixin, a decorator, or a helper assigned as
__pprint__ = pprint.from_attributes(the name is illustrative only). The author of the class then decides whether its attributes are safe to show.A default
object.__pprint__()could be added later, once there is experience with the opt-in. The rule for inherited __pprint__() methods already limits it to classes that do not override__repr__(), sinceobjectcomes last in every method resolution order. It could potentially reveal attributes that the defaultrepr()hides, such as a password or token stored on an instance, in logs and other debugging output, so Security Implications would have to be revisited. It would also be a deliberate exception to the principle that a description agrees withrepr(). And it would need rules for__slots__classes without a__dict__, for attributes whose names start with an underscore, for computed attributes such as those offunctools.cached_property(), and for large object graphs. - A table of markers in the default printer. The default printer handles a fixed set of markers:
BreakableString,BreakableBytesandFormattableNumber.Printercould instead take a table that maps a marker class to a function that renders it, with the built-in markers as its default entries. A user could then replace the built-in markers as well as add new ones, without subclassingPrinter. For example, a class could yield its timestamp wrapped in a marker of its own, and a printer could show it in a chosen format:@dataclass(frozen=True) class Timestamp: value: datetime class Event: def __pprint__(self): yield pprint.KWARGS yield 'name', self.name yield 'when', Timestamp(self.when) def format_timestamp(marker, *, width, last_width, top_level): yield marker.value.strftime('%Y-%m-%d %H:%M') printer = pprint.Printer(markers={Timestamp: format_timestamp})
The names are illustrative only. A function takes the marker and returns an iterable of lines.
widthis the room for each line, from the column where the marker starts, andlast_widthis the room for the last line, which leaves space for any text that follows it on the same line.top_levelis true when the marker is the whole object being printed. The printer places the first line at the current position, and the rest at the marker’s starting column. The widths let a function split its value to fit, as the default printer splits strings, andtop_levellets it parenthesize them. Called with an unlimited width, a function returns the one-line form that the printer uses to decide whether a container fits.A printer would check its table for each object before calling
pprint.decompose(), so any class can be a marker, including a dataclass likeTimestamp, which has a generated__pprint__(). A printer whose table has no entry for a marker prints it like any other object, with its__pprint__()if it has one and withrepr()otherwise.pprint.decompose()itself does not depend on any printer, so it does not know about markers.The reference implementation already renders the built-in markers this way, through a table that is not yet public.
This would go well with a way to change the default printer that
!puses, so that a configured printer reaches plainf'{obj!p}'. A process-wide setting, such aspprint.set_default_printer(), could be paired with a context manager that changes it temporarily. Backing both with acontextvarsvariable would keep threads and asynchronous tasks from seeing each other’s printer.
Open Issues
- Names.
Layoutfor the description of an object’s chrome,pprint.DescriptionTypefor the annotation of a__pprint__()result,pprint.Printerfor the default printer, andhugfor theLayoutattribute are working names. - The exact rule for
hug, which the reference implementation will settle. - Where the new code lives. The authors lean toward implementing the printer in Python, in
pprint. Pretty printing is mostly used for debugging, so the maintainability of the printer matters more than the performance costs of using it. Those costs are the import of a Python module the first time!pis used, and the slower execution of Python code compared with C. - Whether
__pprint__()should take a context argument from the printer. As specified, it takes onlyself, and the printer detects reference cycles and prints therecursiontext of the object’sLayout. A context would move recursion handling into the objects, much asreprlib.recursive_repr()does for__repr__(), at the cost of making every__pprint__()method more complicated. A context would not carry layout state. See Passing layout state to __pprint__(). - Whether
pprint.pprint()andpprint.pformat()should eventually usepprint.Printer. - Whether to add a helper for subclasses that want their base class’s items with their own chrome, such as
a
dictsubclass that prints asMyDict({...}). The helper would run the base class’s__pprint__()and drop itsLayout, if it yields one, so a subclass can yield its ownLayoutfollowed by the rest of the stream. Without it, each subclass has to check for and skip theLayoutitself. The obvious name,pprint.items(), is easily confused with theITEMSmarker, since the helper returns the whole stream, not only key/value items. - Whether the method resolution order rule for
__pprint__()and__repr__()is the right one. - Whether a generated dataclass
__pprint__()should hide fields equal to their defaults. The generatedrepr()shows every field that is not excluded withfield(repr=False), whether or not it equals its default, so for now the generated__pprint__()does not hide them either. - How a class can add
hintsto the__pprint__()method thatdataclassesgenerates. Adataclassoption, such aspprint_hints=, could supply them. So could a helper that runs the generated method and replaces itsLayout, like the helper for base classes above. - Whether the interactive interpreter should display an object in its pretty printed form, rather than with
its
repr(), when its type has a usable__pprint__(). Since the built-in containers implement__pprint__(), this would change how the REPL displays most results. The authors think it would be very handy, but it has a cost. Pretty printed output depends on the printer’s layout algorithm and on the width of the terminal, and either can change, so REPL output could no longer be pasted into a doctest that works across Python versions. Turning it on by default would therefore need a way to turn it off, such as a-X pretty=0option with a matchingPYTHON_PRETTYenvironment variable, and asys.flagsentry that reports it.One way to do this is for the REPL to install a pretty printing
sys.displayhook()at startup, unless the option turns it off.sys.__displayhook__would keep usingrepr(), so a session could switch back at any time withsys.displayhook = sys.__displayhook__.doctestalready runs examples withsys.__displayhook__, so doctests themselves would be unaffected. - Whether to add a built-in way to pretty print a value without an f-string. The choices include:
- A
prettyargument to theprint()built-in function, as an earlier draft of this PEP proposed.print(obj, pretty=True)would use the default printer, andprint(obj, pretty=printer)the given printer, for each of the positional arguments. - A new
pprint()built-in function. Its name would be shadowed by the commonfrom pprint import pprint, which usesPrettyPrinterrather than the default printer, so the two would print the same object differently.
print(f'{obj!p}')andprint(f'{obj!p:{printer}}')already do this, but either choice would be shorter, and could help with pretty printed REPL output, described above. - A
- Whether
pprint.decompose()should set the default chrome attributes in theLayoutit returns. As specified, it returns aLayoutwith every attribute set, so a printer cannot tell chrome that an object chose from the defaults. For example,listchoosesopen='['andclose=']', whileBass, which yields noLayout, gets the defaultsopen='Bass('andclose=')'. A printer for another format, such as HTML, may want to keep the chrome an object chose but replace the defaults with its own, for example by showing the name as a heading.pprint.decompose()could instead leave the attributes that an object did not set asNone, and let each printer supply its own defaults. - Sorting for other printers. The rules for sorting described in Nested objects live in the default
printer, so every other printer that honors
sortablehas to reimplement them.pprintcould provide a public helper for sorting items, orsortablecould be advisory, with each printer free to sort as it likes.
Acknowledgments
Pablo Galindo Salgado for helping the PEP authors prototype the use of and prove the feasibility of
!p with a format spec for f-strings.
Larry Hastings for his feedback and suggestions during the PEP and reference implementation process.
Bartosz Sławecki for suggesting that the REPL display objects in their pretty printed form.
The participants of the October 2026 core developer sprint, for discussing the redesign of the protocol.
Footnotes
None at this time.
Change History
- 24-Oct-2026
- Add a new default printer,
pprint.Printer, and leavepprint.PrettyPrinterunchanged. __pprint__()describes an object’s structure, and the printer makes all layout decisions.- Section markers
pprint.VALUES,pprint.KWARGSandpprint.ITEMSsay how tuples are read, replacing the reading of tuples by their shape and the “false-y” name for positional tuple values. - Add
pprint.Layoutfor an object’s chrome, thepprint.DELEGATEmarker, and thepprint.Keywordandpprint.Entrytypes. The markers are PEP 661 sentinels. - Specify that the default printer reads the stream permissively, and that other printers may define their own vocabulary.
- Add
pprint.decompose(), and thepprint.LEAFsentinel that it returns for a leaf. - Defer a table of markers in the default printer, and a way to change the default printer.
- Add
pprint.DescriptionTypefor type annotating__pprint__()methods. - Add an optional
hintsattribute topprint.Layout, which can be used by third-party printers. - Add a
hugattribute topprint.Layout, so thatdefaultdictand similar types keepPrettyPrinter’s layout. - A printer returns an iterable of lines, and
!pjoins them with'\n'. - Specify how the default printer lays out, sorts, and limits the depth of nested objects, including keys that wrap.
- The default printer’s
expandoption takes apprint.Expandmember:NEVER,FITorALWAYS. Its defaults arewidth=78,indent=4andexpand=Expand.FIT. - The default printer’s
compactoption works with everyexpandmode. - The built-in and standard library types implement
__pprint__().strandbytesdescribe themselves with the newpprint.BreakableStringandpprint.BreakableBytes, andintandfloatwith the newpprint.FormattableNumber, so the printer handles no type natively. - The default printer’s
number_formatoption replacesunderscore_numbers. - Specify when an inherited
__pprint__()is used, and explain why. dataclassesandnamedtuple()generate__pprint__()methods.- The
!pformat spec can be a single replacement field,f'{obj!p:{printer}}', whose value is called without first being converted to a string. Any other spec is aSyntaxError. - A printer of
Nonemeans the default printer, in f-strings, t-strings andPyObject_Pretty(). - In
str.format(), a format spec after!ppads the pretty printed string. - T-strings support
!p.Interpolationgets aprinterattribute, andstring.templatelib.convert()handles'p'. - Describe compatibility with Rich, IPython and other pretty printing protocols.
- Move the Rationale section after the Specification, as PEP 12 now recommends.
- Add a new default printer,
- 04-Mar-2026
- For f-strings only (not
str.format()) the!pconversion specifier takes an optional “format spec”. - The PEP no longer proposes a
prettyargument to theprint()built-in function. With the addition of!p:callablesyntax for f-strings, the new argument is unnecessary. - Specify that to pretty print tuples as positional arguments, use the 2-tuple value format, passing a “false-y” value as the argument name.
- Clarify that a truth-y
namemust be astr. - Specify that the
!pconversion in f-strings andstr.format()implicitly perform an import of thepprintmodule. - Describe the new Limited C API function
PyObject_Pretty(), and add the optional argument.
- For f-strings only (not
Copyright
This document is placed in the public domain or under the CC0-1.0-Universal license, whichever is more permissive.