Skip to main content

A modern pprint alternative — pretty-print any Python object as clean JSON, with zero dependencies

Project description

ppobj

A modern alternative to pprint. Pretty-print any Python object as clean, formatted JSON — including custom classes, HTTP responses, and objects that crash standard serializers.

pip install ppobj

Why ppobj?

Python's pprint handles standard containers and dataclasses well, but it can't look inside custom objects (just shows <object at 0x...>), can't consume generators, can't parse HTTP response bodies, and crashes on objects with broken __repr__. Its output is Python repr syntax — not valid JSON, not pasteable into APIs or config files.

ppobj solves all of this:

  • Inspects any object — extracts __dict__ and __slots__ attributes recursively, no __repr__ needed
  • Valid JSON output — double quotes, true/false/null, pasteable and parseable everywhere
  • Never crashes — unserializable fields become descriptive placeholders, not exceptions
  • Zero dependencies — pure stdlib at runtime
  • Two lines of codefrom ppobj import pp then pp(anything)

Quick Start

from ppobj import pp

pp({"name": "Alice", "age": 30, "hobbies": ["reading", "coding"]})
{
  "name": "Alice",
  "age": 30,
  "hobbies": [
    "reading",
    "coding"
  ]
}

ppobj vs pprint

See the full side-by-side comparisons in examples/vs_pprint/.

Custom objects

pprint shows <object at 0x...>. ppobj extracts the actual attributes:

class Config:
    def __init__(self):
        self.host = "localhost"
        self.port = 5432
        self.debug = True
pprint ➜  <__main__.Config object at 0x7f3a...>

ppobj  ➜  {
            "__type__": "Config",
            "host": "localhost",
            "port": 5432,
            "debug": true
          }

The __type__ key is included by default so you know what class the object is. To omit it:

printer = PrettyPrinter(show_type=False)
printer.pp(Config())
# {"host": "localhost", "port": 5432, "debug": true}

Generators

pprint shows <generator object ...>. ppobj consumes and shows the values:

pp(x ** 2 for x in range(5))
pprint ➜  <generator object <genexpr> at 0x7f3a...>

ppobj  ➜  [0, 1, 4, 9, 16]

JSON output with compact mode

pprint outputs Python repr syntax (single quotes, True/False/None) — not valid JSON. ppobj outputs standard JSON, with optional width-aware compaction:

pp({"name": "Alice", "age": 30}, compact=True)
# {"name": "Alice", "age": 30}    ← valid JSON, pasteable anywhere

pp(large_nested_object, width=60)
# Outer structure expands, leaf dicts/lists stay compact

Safety

pprint crashes on objects with broken __repr__. ppobj never crashes:

class Broken:
    def __init__(self):
        self.value = 42
    def __repr__(self):
        raise RuntimeError("repr is broken!")

pp(Broken())
# {"__type__": "Broken", "value": 42}

Run all comparison examples with:

uv run python examples/vs_pprint/01_custom_objects.py
uv run python examples/vs_pprint/02_compact_formatting.py
uv run python examples/vs_pprint/03_dataclasses_enums.py
uv run python examples/vs_pprint/04_safety_and_edge_cases.py
uv run python examples/vs_pprint/05_http_responses.py        # requires network
uv run python examples/vs_pprint/06_generators_iterators.py

HTTP Responses in Seconds

Debugging API calls? Just pp() the response — headers, parsed JSON body, status, timing, all laid out:

import httpx
from ppobj import pp

response = httpx.get("https://httpbin.org/json")
pp(response)
{
  "__type__": "httpx.Response",
  "status_code": 200,
  "url": "https://httpbin.org/json",
  "reason_phrase": "OK",
  "http_version": "HTTP/1.1",
  "is_redirect": false,
  "headers": {
    "content-type": "application/json"
  },
  "body": {
    "slideshow": {
      "author": "Yours Truly",
      "title": "Sample Slide Show"
    }
  },
  "elapsed": "0:00:00.900709"
}

Works with both httpx and requests — no extra imports needed.

API

from ppobj import pp, to_json, to_dict

# Print to stdout (returns the original object for inline debugging)
result = process(pp(data))

# Compact mode — collapses small structures onto one line
pp(obj, compact=True)
pp(obj, width=60)

# Formatting options
pp(obj, indent=4, sort_keys=True)

# Get as JSON string
json_str = to_json(obj)

# Get as dict/list/primitive for further processing
data = to_dict(obj)

PrettyPrinter class

For reusable, configured output:

from ppobj import PrettyPrinter

printer = PrettyPrinter(
    indent=4,
    width=100,
    compact=True,
    max_depth=10,
    max_items=500,
    max_string_length=5000,
    sort_keys=True,
)
printer.pp(my_object)

Custom handlers

Extend ppobj with your own type serializers:

from ppobj import BaseHandler, PrettyPrinter

class MyTypeHandler(BaseHandler):
    def can_handle(self, obj):
        return isinstance(obj, MySpecialType)

    def serialize(self, obj, *, recurse, context):
        return {"custom_field": recurse(obj.data)}

printer = PrettyPrinter()
printer.registry.prepend(MyTypeHandler())  # highest priority

Supported Types

Category Types
Primitives str, int, float, bool, None
Containers dict, list, tuple, set, frozenset
Collections OrderedDict, defaultdict, Counter, deque, namedtuple
Date/Time datetime, date, time, timedelta
Enums Enum, IntEnum, StrEnum, Flag
Dataclasses @dataclass (including frozen and slotted)
Exceptions All Exception subclasses with chaining info
HTTP httpx.Response, requests.Response
Pydantic BaseModel v1 and v2
Stdlib Path, UUID, Decimal, re.Pattern, bytes, complex, range
Iterators Generators, map, filter, zip, custom iterators
Custom Any object with __dict__ or __slots__

No library imports required — ppobj detects types by structure, not by importing the library.

Graceful Error Handling

ppobj never raises on bad data. When a field can't be serialized, you get a clear placeholder:

{
  "normal_field": "works fine",
  "problematic_field": "<unserializable: SomeType, len=3, repr=<SomeType object>>"
}

Other safety placeholders:

Placeholder Meaning
<circular reference: ClassName> Circular reference detected
<max depth (20) exceeded: ClassName> Nesting too deep
<string: 5000 chars, truncated> String exceeded max_string_length
<... 990 more items (total: 1000)> Collection exceeded max_items
<not yet consumed> HTTP response body not read yet
<stream not consumed> Streaming response not consumed

Requirements

  • Python 3.10+
  • Zero runtime dependencies

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

ppobj-1.0.0.tar.gz (41.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

ppobj-1.0.0-py3-none-any.whl (26.5 kB view details)

Uploaded Python 3

File details

Details for the file ppobj-1.0.0.tar.gz.

File metadata

  • Download URL: ppobj-1.0.0.tar.gz
  • Upload date:
  • Size: 41.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for ppobj-1.0.0.tar.gz
Algorithm Hash digest
SHA256 d33960aa66b25f8cd6235e1237aaf8467a2a25ff1bd894ccf1a7c2658f58a2b3
MD5 53021126efd2cde4d1b3b6ff61ff664e
BLAKE2b-256 225b2643fa16152ba12ada1181c528d3ca85888bfa680449e6c757a6ae4fadce

See more details on using hashes here.

File details

Details for the file ppobj-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: ppobj-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 26.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.3

File hashes

Hashes for ppobj-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2e7b7b3bd4aecfbe5fc8da278c22b7352fa461bf5f5c611cae535fe9e7bf55a0
MD5 49049f5ece70e7dbe841710c24582711
BLAKE2b-256 166ff1ba0248901af2b08182a2c1f6ecbcefce89ca19a8da741091948e58fb61

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page