Skip to content

PEP 813: bring the implementation in line with the PEP draft - #2

Merged
ericvsmith merged 4 commits into
ericvsmith:PEP-813-pprintfrom
warsaw:pep813-fixes
Oct 23, 2026
Merged

ericvsmith merged 4 commits into
ericvsmith:PEP-813-pprintfrom
warsaw:pep813-fixes

Conversation

@warsaw

@warsaw warsaw commented Oct 23, 2026

Copy link
Copy Markdown

This brings the branch in line with the current PEP 813 draft (python/peps#5191). Each commit has the details.

  • Restore the public PyObject_Pretty() in the limited C API (I think its removal was accidental). It joins the printer's lines and returns a str, like PyObject_Str(). It raises TypeError if the printer returns a str, which used to give one line per character, or a line that isn't a str. FORMAT_PRETTY and str.format() both call it.
  • Fix a dataclass bug: a dataclass that defines its own __repr__() no longer gets a generated __pprint__(). The generated method won under the MRO rule, so it bypassed the hand-written repr(), including one that hides a secret field.
  • Public names: the protocol's names are re-exported from pprint, lazily, so import pprint doesn't import _pprint or dataclasses.
  • New in the PEP and added here:
    • pprint.LEAF, which decompose() returns as (LEAF, None) for a leaf.
    • pprint.DescriptionType.
    • pprint.FormattableNumber, used by int and float, together with a number_format option that replaces underscore_numbers. It's a format spec, such as '_' or ',', and the printer falls back to repr() if format() raises.
    • Layout.hug, which keeps defaultdict(<class 'list'>, { and similar on one line, as PrettyPrinter(expand=True) does.
  • String and bytes splitting now happens at render time, so it depends on the column, as in PrettyPrinter. Only a top-level literal is parenthesized.
  • Output matching PrettyPrinter: bytearray, deque (including maxlen), mappingproxy, the MappingViews and defaultdict, plus strings and bytes inside containers.
  • Matching repr(): tuple recursion now prints (...), and Interpolation is described positionally.
  • Programs/test_frozenmain.h is regenerated for the new opcode, which fixes test_embed.

Testing

  • The new tests in test__pprint compare the output with pprint.pformat() for these cases, in both modes.
  • New tests in test_fstring cover the printer result checks, and test_dataclasses covers the __repr__() bug.
  • The full test suite passes, except test_remote_pdb, which hung in my environment and was stopped. It doesn't touch any of this code.

🤖 Generated with Claude Code

warsaw and others added 4 commits October 23, 2026 10:10
PEP 813 specifies PyObject_Pretty() in the limited C API.  It calls the
printer (the default one when printer is NULL), joins the lines it returns
with '\n', and returns the str, mirroring PyObject_Str() and friends.  It
raises TypeError if the printer returns a str, whose items are characters
rather than lines, or if any line is not a str.

FORMAT_PRETTY and str.format() now call it, so the join and its checks live
in one place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…__().

Both methods would be defined on the same class, so the MRO rule would use
the generated __pprint__(), which describes every field, instead of the
hand-written __repr__(), which may hide some of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Re-export the protocol's names from pprint, lazily, so that importing
  pprint stays cheap.
* Add pprint.LEAF, which decompose() returns for a leaf, and
  pprint.DescriptionType.
* Add pprint.FormattableNumber.  int and float delegate to it, and the
  Printer formats it with its new number_format option, a format spec.
  If format() raises, the number is printed with repr().
* Add a Layout.hug attribute.  When exactly one item is a container, the
  other items stay on the opening and closing lines, as PrettyPrinter does
  with expand=True.  defaultdict, mappingproxy, the MappingViews and deque
  use it.
* Split strings and bytes when rendering, so that the split depends on the
  column, as in PrettyPrinter.  Only a literal that is the whole object is
  parenthesized.  The continuation lines of a nested literal are aligned
  under its first piece.
* Match PrettyPrinter for bytearray and deque, print OrderedDict as
  OrderedDict({...}), use '(...)' for tuple recursion, and describe an
  Interpolation positionally, to agree with repr().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This fixes test_embed's test_frozenmain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@warsaw
warsaw requested a review from ericvsmith as a code owner October 23, 2026 17:10
@warsaw

warsaw commented Oct 23, 2026

Copy link
Copy Markdown
Author

What's left after this PR

These are the remaining differences from the PEP draft, and questions for the two of us.

To do

  1. T-strings: keep the printer object. !p is allowed in t-strings, as decided, but t'{x!p:{printer}}' stores str(printer) as Interpolation.format_spec, so the printer is lost. There are two ways to keep it:

    • Store the printer object itself in format_spec when the conversion is 'p'. This keeps the four attributes and __match_args__, but format_spec is documented as a str.
    • Add a new attribute, such as Interpolation.printer.

    Two related gaps:

    • string.templatelib.convert(x, 'p') raises ValueError. It should use the default printer, perhaps with an optional printer argument.
    • The Python-level Interpolation(value, expr, 'p') raises ValueError, and its message lists only 's', 'a' and 'r'.

    The PEP will describe t-string support once this settles.

  2. The exact rule for hug. The name and rule here are working versions, and the PEP lists them as an Open Issue. The rule: when the object breaks and exactly one of its items is a non-empty container, the other items stay on the opening and closing lines. It applies only to FIT and ALWAYS. It covers defaultdict, mappingproxy, KeysView and deque(maxlen=...), but propose something else if you see a better rule.

Differences from PrettyPrinter that this PR makes on purpose

  • OrderedDict now always prints as OrderedDict({...}). That matches repr(), and PrettyPrinter's one-line form. When it wraps, it looks like Counter, while PrettyPrinter switches to OrderedDict([('a', 1), ...]). The protocol can't describe one form for a single line and another when it wraps. Barry will confirm this choice.
  • Interpolation is positional, to agree with repr(). PrettyPrinter itself is inconsistent here: positional with expand=False, keywords with expand=True.
  • A long string that's a dict or keyword value, in FIT: its continuation lines are aligned under the first piece. PrettyPrinter puts them at the item's indentation. The PEP lists this as a deliberate exception.
  • Named tuples now wrap, because they have a __pprint__(). PrettyPrinter prints them on one line.

Open question

  • indent in NEVER. The printer ignores indent in NEVER and aligns items under the open bracket. PrettyPrinter(expand=False) uses indent as padding after the bracket, [ 'a', at indent=4. So the two match only at indent=1, which is PrettyPrinter's default but not Printer's.

Performance

These are timings of Printer()(obj) for 100,000-element lists, best of 3:

list(range(100_000)) 0.51 s
100,000 floats 0.52 s
[None] * 100_000, leaves with no __pprint__() 0.38 s
pprint.pformat(list(range(100_000))) 0.08 s

So the FormattableNumber delegation adds about a third over a plain leaf. The bigger gap is that the document-based printer is about 6× slower than PrettyPrinter overall. The PEP accepts some cost for maintainability, but the renderer may be worth profiling before this goes upstream.

🤖 Generated with Claude Code

@ericvsmith
ericvsmith merged commit a91d641 into ericvsmith:PEP-813-pprint Oct 23, 2026
14 of 27 checks passed
@warsaw
warsaw deleted the pep813-fixes branch October 23, 2026 19:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants