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 code —
from ppobj import ppthenpp(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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d33960aa66b25f8cd6235e1237aaf8467a2a25ff1bd894ccf1a7c2658f58a2b3
|
|
| MD5 |
53021126efd2cde4d1b3b6ff61ff664e
|
|
| BLAKE2b-256 |
225b2643fa16152ba12ada1181c528d3ca85888bfa680449e6c757a6ae4fadce
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2e7b7b3bd4aecfbe5fc8da278c22b7352fa461bf5f5c611cae535fe9e7bf55a0
|
|
| MD5 |
49049f5ece70e7dbe841710c24582711
|
|
| BLAKE2b-256 |
166ff1ba0248901af2b08182a2c1f6ecbcefce89ca19a8da741091948e58fb61
|