Pretty Print JSON in Python: A Practical, Modern Guide

When I’m debugging an API response or reviewing a config export, raw JSON can feel like staring at a wall of text. One missing brace and your eyes glaze over. Pretty printing is the small step that makes JSON readable, shareable, and safe to reason about—especially when you’re comparing versions, reviewing logs, or writing unit tests. I’ve seen teams lose hours to a misplaced comma simply because the output was compacted into a single line. I treat pretty printing as a default habit rather than a special trick.

You’ll get a clear, modern playbook for pretty printing JSON in Python: when to use it, when not to, how to format consistently across tools, and how to keep it fast in real systems. I’ll cover json.dumps, file workflows, custom encoders, handling datetimes and Decimals, and dealing with huge payloads without melting your terminal. You’ll also see common mistakes I watch for in code reviews and a few patterns I’ve adopted in 2026 workflows where AI copilots help draft data transformations but humans still need clarity when verifying output.

Why pretty printing matters in real work

Pretty printing is not just for tutorials. I use it in three recurring situations: debugging, change reviews, and client-facing exports. When your API returns 2000 lines of nested data, the raw single-line JSON is useless. Pretty output helps you spot what changed between two versions of a payload, makes diffs smaller and more reliable, and lowers the risk of shipping bad data.

I also see a pragmatic compliance angle. If you’re validating that personally identifiable fields are removed before logging, you need to visually scan the output. A compact payload hides those issues. Pretty printing gives you a readable snapshot you can review in seconds.

Another reason: your future self. I can’t count how many times I’ve opened an old log file from a production incident and felt relieved it was readable. The few milliseconds saved by compact output are rarely worth the hours wasted in analysis later.

The core tool: json.dumps with indent

Python’s built-in json module is enough for most cases. The simplest pattern is json.dumps(data, indent=2) for strings or json.dump(data, file, indent=2) for files. I typically use an indent of 2 or 4 depending on the project’s style guide. Two spaces fit well in logs; four spaces work better in docs and config files.

Here’s a complete, runnable example using a realistic employee payload, with a note about indentation consistency:

import json

json_data = ‘[{"Employee ID":1,"Name":"Abhishek","Designation":"Software Engineer"},‘ \

‘{"Employee ID":2,"Name":"Garima","Designation":"Email Marketing Specialist"}]‘

jsonobject = json.loads(jsondata)

I prefer indent=2 for logs because it stays compact but readable.

print(json.dumps(json_object, indent=2))

If you change indent from 2 to 4, the output becomes more spread out. There’s no magic number; the best choice is the one your team sticks to. I usually standardize this in a shared json_format.py or in a logging helper so nobody argues about tabs versus spaces at 2 a.m.

Key formatting knobs you should know

indent is the main lever, but there are a few other settings I reach for:

  • sort_keys=True: Keeps the output stable by sorting keys alphabetically. I use this for diffs and tests.
  • ensure_ascii=False: Preserves Unicode characters. If you work with names like “Søren” or “Zoë,” this matters.
  • separators=(‘,‘, ‘: ‘): Controls spacing around separators. Useful if you want compact output but still readable.

Example with deterministic ordering for tests:

import json

data = {

"region": "North America",

"team": "Platform",

"active": True,

"members": ["Ava", "Noah", "Maya"]

}

print(json.dumps(data, indent=2, sort_keys=True))

I use this pattern in snapshot tests so diffs are clean. If keys are random, your test snapshots become noise.

Pretty printing JSON from files

Reading from a file is common in ETL pipelines, config validation, and log analysis. I prefer context managers because they reduce file descriptor leaks and errors. This is a good baseline pattern:

import json

with open("employees.json", "r", encoding="utf-8") as f:

data = json.load(f)

print(json.dumps(data, indent=2))

If you want to write the pretty output back to a file, just use json.dump:

import json

payload = {

"emp1": {"name": "Lisa", "designation": "programmer", "age": 34, "salary": 54000},

"emp2": {"name": "Elis", "designation": "Trainee", "age": 24, "salary": 40000},

}

with open("employees_pretty.json", "w", encoding="utf-8") as f:

json.dump(payload, f, indent=2, sort_keys=True)

Notice that I store numeric fields as numbers, not strings. It’s a subtle habit that pays off when you’re later filtering or aggregating data.

Handling non-JSON types (datetime, Decimal, Path)

The json module only knows about basic types: dict, list, str, int, float, bool, and None. Real-world data often includes datetimes, Decimals, or file paths. If you try to dump those directly, you’ll get a TypeError. I solve this by creating a custom encoder.

import json

from datetime import datetime, timezone

from decimal import Decimal

from pathlib import Path

class PrettyEncoder(json.JSONEncoder):

def default(self, obj):

if isinstance(obj, datetime):

return obj.astimezone(timezone.utc).isoformat()

if isinstance(obj, Decimal):

# Convert to string to avoid float precision issues

return str(obj)

if isinstance(obj, Path):

return str(obj)

return super().default(obj)

payload = {

"invoice_id": "INV-2026-0042",

"issued_at": datetime(2026, 1, 26, 14, 30, tzinfo=timezone.utc),

"total": Decimal("199.95"),

"attachment": Path("/data/invoices/INV-2026-0042.pdf")

}

print(json.dumps(payload, indent=2, cls=PrettyEncoder))

I recommend using Decimal for currency and keeping the value as a string in JSON to avoid floating point confusion. When I review bugs around rounding, most originate from float conversions that looked harmless at the time.

An encoder pattern I reuse across projects

I often wrap the encoder plus settings in a single helper. This keeps my projects consistent and lets me extend behavior without hunting through the codebase.

import json

from datetime import datetime, date, timezone

from decimal import Decimal

from pathlib import Path

class SafeJSONEncoder(json.JSONEncoder):

def default(self, obj):

if isinstance(obj, (datetime, date)):

if isinstance(obj, datetime):

obj = obj.astimezone(timezone.utc)

return obj.isoformat()

if isinstance(obj, Decimal):

return str(obj)

if isinstance(obj, Path):

return str(obj)

return super().default(obj)

def pretty_json(obj) -> str:

return json.dumps(

obj,

indent=2,

ensure_ascii=False,

sort_keys=True,

cls=SafeJSONEncoder,

)

This version also handles date objects and enforces UTC for datetime. It’s small, but it eliminates repeated boilerplate in every script.

Dealing with large JSON payloads without freezing your tools

Pretty printing a 200MB payload can make your terminal sluggish or even crash your editor. I’ve run into this in data exports and log analysis. Here are two strategies I use:

1) Stream and preview

Instead of loading everything into memory, I preview the top-level structure and a few items. That keeps memory usage manageable.

import json

with open("big_payload.json", "r", encoding="utf-8") as f:

data = json.load(f)

Safely preview the first 2 entries if data is a list

if isinstance(data, list):

preview = data[:2]

else:

# Preview a few keys if data is a dict

preview = {k: data[k] for k in list(data.keys())[:3]}

print(json.dumps(preview, indent=2))

2) Use log-friendly compacting with separators

When I need readable output but can’t afford lots of whitespace, I use compact separators with a small indent or no indent. It’s a middle ground.

import json

print(json.dumps(data, indent=1, separators=(",", ": ")))

This keeps arrays readable without adding too many lines. I usually do this inside debug logs so I can still scan without bloating log files.

3) Chunked writing for long lists

If you have a huge list of items, you can write a JSON array manually in chunks to avoid holding it all in memory at once. This is more advanced but useful for exports.

import json

items = [{"id": i, "value": i * 2} for i in range(5)]

with open("chunked.json", "w", encoding="utf-8") as f:

f.write("[\n")

for i, item in enumerate(items):

if i > 0:

f.write(",\n")

f.write(json.dumps(item, indent=2))

f.write("\n]\n")

This isn’t perfect for all cases, but it’s a simple way to keep output pretty without a huge memory spike.

Common mistakes I see (and how I avoid them)

I do a lot of code review, and these are the recurring issues that cause confusion:

1) Pretty printing and then re-parsing: Some developers call json.dumps and immediately json.loads to “clean” data. That’s wasted work and can introduce subtle type changes. I only pretty print for output, not for transformations.

2) Forgetting ensureascii=False: If your data includes non-English names, pretty output becomes full of escape codes. I explicitly set ensureascii=False when I expect international text.

3) Accidental string types: People read JSON as strings and then dump the string itself, which results in extra quotes and escaped characters. If you see output like "{\"id\": 1}", you’re pretty printing a string instead of a parsed object.

4) Huge indentation in logs: I’ve seen logs with indent=8. It looks nice but makes logs enormous and slower to ship. I standardize indent=2 for logs, indent=4 for documents.

5) Non-deterministic output in tests: Snapshot tests fail randomly when key order changes. I always set sort_keys=True in tests.

If you want a single safe default that avoids most of these errors, use this:

def pretty_json(obj) -> str:

return json.dumps(obj, indent=2, ensureascii=False, sortkeys=True)

When to pretty print vs when not to

Pretty printing is a tool, not a default for every output. I decide based on the audience and the medium.

Use pretty printing when:

  • You’re debugging or troubleshooting
  • You’re writing human-facing documentation
  • You’re producing config or fixture files
  • You’re generating a diff or a snapshot test

Avoid pretty printing when:

  • You’re streaming high-volume logs in production
  • You need to keep output size minimal (like mobile data sync)
  • You’re measuring latency in high-throughput services

I’ll still log compact JSON in production, but I’ll add a “pretty mode” toggle or a feature flag for deep debugging. That way I can switch on readability when I need it without paying the cost every time.

Traditional vs modern workflows (2026 perspective)

The core mechanics of pretty printing haven’t changed, but the workflows around it have. AI assistants can generate JSON transformations quickly, but I always verify the output manually before trusting it. Pretty printing becomes my “visual checksum.”

Here’s a clear comparison that I use when helping teams decide how to handle JSON formatting in their pipelines:

Approach

Traditional Pattern

Modern Pattern (2026)

When I Choose It

Local debug

print(json.dumps(obj, indent=2))

prettyjson(obj) helper + editor snippet

Quick checks, local experiments

Tests

Unsorted dumps in snapshots

sort
keys=True with stable formatting

CI stability and predictable diffs

Logs

Compact JSON, unreadable

Compact by default, pretty behind flag

Production safety with on-demand clarity

Tooling

Manual scripts

AI-assisted transformation + human review

Rapid iteration with verificationEven with smarter tooling, I stick to a simple rule: readable output first, minimal output only when there’s a performance reason.

Performance considerations you should account for

Pretty printing adds whitespace, and that means bigger output and more CPU time. The overhead is usually modest for small objects, but it adds up with big payloads.

Here’s how I think about it in practice:

  • Small objects (dozens of keys): overhead is negligible, typically 1–5ms in local tests.
  • Medium payloads (thousands of keys): overhead can be noticeable, typically 10–30ms depending on hardware.
  • Large payloads (hundreds of thousands of keys): overhead can be seconds. I avoid pretty printing unless it’s critical.

If you need both performance and readability, consider:

  • pretty printing only in debug mode
  • pretty printing a subset of the payload
  • sampling requests (e.g., 1 in 100 logs)

I treat pretty printing as a readability feature, and I pay for it only when I get the benefit back.

A simple, practical micro-benchmark

I don’t over-optimize pretty printing, but if I’m curious I’ll run a quick benchmark using timeit.

import json

import timeit

payload = {"key": list(range(1000)), "nested": {"a": 1, "b": 2, "c": 3}}

print(timeit.timeit(lambda: json.dumps(payload), number=1000))

print(timeit.timeit(lambda: json.dumps(payload, indent=2), number=1000))

This isn’t production-grade benchmarking, but it gives you a directional feel for the cost of whitespace. If the difference is irrelevant, I keep the code readable.

Practical patterns I use in production code

Below are two small patterns that show how I integrate pretty printing without creating chaos.

1) A dedicated formatter utility

This keeps all formatting in one place, making it easy to change later.

import json

PRETTYJSONINDENT = 2

def pretty_json(obj) -> str:

return json.dumps(

obj,

indent=PRETTYJSONINDENT,

ensure_ascii=False,

sort_keys=True,

)

I keep this in a utils/json_format.py module and import it whenever I need readable JSON. It’s a small layer, but it prevents duplication.

2) Logger integration with a debug flag

I often have a logger that can switch between compact and pretty output.

import json

DEBUG_JSON = False

def log_json(obj):

if DEBUG_JSON:

print(json.dumps(obj, indent=2, ensure_ascii=False))

else:

print(json.dumps(obj, separators=(",", ":"), ensure_ascii=False))

You can plug this into a real logging framework instead of print. I keep it simple here because the idea matters more than the library.

Edge cases that surprise people

Here are a few tricky cases I’ve seen in real systems:

  • NaN and Infinity: Python allows them by default, but JSON standard does not. Use allow_nan=False to enforce strict compliance. If you see ValueError: Out of range float values are not JSON compliant, that’s why.
  • Circular references: If an object references itself, json.dumps will blow up. You need to pre-process the data or use a custom serializer.
  • Mixed key types: JSON keys must be strings. If you build dicts with non-string keys, Python will coerce them, often in unexpected ways.

Example with strict float handling:

import json

import math

data = {"score": math.nan}

try:

print(json.dumps(data, allow_nan=False))

except ValueError as e:

print("JSON compliance error:", e)

I enable allow_nan=False in data exports to avoid writing invalid JSON to downstream systems.

Handling circular references safely

There’s no built-in json solution for circular references, but I sometimes sanitize data with a helper that replaces repeats with a marker.

import json

class SafeCycleEncoder(json.JSONEncoder):

def init(self, args, *kwargs):

super().init(args, *kwargs)

self._seen = set()

def default(self, obj):

obj_id = id(obj)

if objid in self.seen:

return ""

self.seen.add(objid)

return json.JSONEncoder.default(self, obj)

This is intentionally simple and not suited for all objects, but it prevents infinite recursion in messy debugging situations.

Pretty printing in interactive workflows

If you’re working in notebooks or REPL sessions, you can use json.dumps directly, but I prefer a helper that auto-prints. It reduces repetitive typing.

import json

def pp(obj):

print(json.dumps(obj, indent=2, ensureascii=False, sortkeys=True))

pp({"project": "Phoenix", "status": "active", "members": ["Kai", "Riya"]})

In Jupyter, I often use IPython.display.JSON for richer formatting, but the core principle stays the same: readability without surprises.

A small REPL-friendly trick

I sometimes add a one-liner in my REPL session that binds pretty printing to a short name so I can use it constantly while exploring data:

import json

pp = lambda obj: print(json.dumps(obj, indent=2, ensureascii=False, sortkeys=True))

It’s not fancy, but it makes interactive exploration smoother.

Putting it all together: a complete example

This example shows a realistic workflow: read a JSON file, normalize some fields, pretty print with stable ordering, and write the result back. It’s simple but represents a pattern I use when cleaning API outputs before committing them to version control.

import json

from datetime import datetime, timezone

INPUTPATH = "rawexport.json"

OUTPUTPATH = "prettyexport.json"

with open(INPUT_PATH, "r", encoding="utf-8") as f:

data = json.load(f)

Normalize a timestamp if it exists

if "exportedat" in data and isinstance(data["exportedat"], str):

data["exported_at"] = datetime.now(timezone.utc).isoformat()

with open(OUTPUT_PATH, "w", encoding="utf-8") as f:

json.dump(data, f, indent=2, ensureascii=False, sortkeys=True)

I like this pattern because it makes the output consistent and version-control friendly.

New: practical scenarios you can reuse today

Pretty printing becomes more valuable when you treat it as part of a workflow, not just a quick trick. Here are some scenarios I rely on.

Scenario 1: Debugging API responses with selective visibility

Sometimes I don’t want the entire payload, just the shape and a few entries. I use a trimming helper so I can keep output readable without deleting data.

import json

def preview(obj, listitems=2, dictkeys=4):

if isinstance(obj, list):

return obj[:list_items]

if isinstance(obj, dict):

return {k: obj[k] for k in list(obj.keys())[:dict_keys]}

return obj

response = {

"status": "ok",

"meta": {"page": 1, "limit": 50},

"items": [{"id": i, "name": f"Item {i}"} for i in range(10)]

}

print(json.dumps(preview(response), indent=2))

This lets me quickly validate structure without drowning in details.

Scenario 2: Comparing two JSON payloads in tests

I often compare payloads by pretty printing them and writing them to temp files. That way, if a test fails, the diff is clear.

import json

def stable_dump(obj, path):

with open(path, "w", encoding="utf-8") as f:

json.dump(obj, f, indent=2, sortkeys=True, ensureascii=False)

expected = {"a": 1, "b": [2, 3]}

actual = {"b": [2, 3], "a": 1}

stable_dump(expected, "expected.json")

stable_dump(actual, "actual.json")

Even if the data is logically the same, the formatting ensures you can see real differences without noise.

Scenario 3: Prettifying JSON from a command line pipe

Sometimes you don’t want to write a full script. If you have a JSON file in a pipeline, you can pretty print it quickly with a one-liner:

import json, sys

print(json.dumps(json.load(sys.stdin), indent=2, ensure_ascii=False))

I keep this in a small script so I can do: cat data.json | python pretty.py.

New: deeper explanation of formatting controls

Most people use indent and stop there. But the other knobs can matter in subtle ways.

separators and whitespace control

The default separator is (‘, ‘, ‘: ‘). If you want compact output that still feels readable, you can tighten the comma spacing and keep a space after :.

import json

data = {"id": 1, "name": "Ravi", "active": True}

print(json.dumps(data, separators=(",", ": ")))

That yields something like {"id":1,"name":"Ravi","active":true} in a single line. If you add indent, the separators still matter for how trailing spaces are handled on each line.

sort_keys for deterministic diffs

I treat sort_keys=True as a policy decision. It’s great for tests and configs, but it can be surprising when you want to preserve semantic order. For example, if your JSON represents a UI schema where order matters, sorting keys might hurt readability.

I decide based on intent:

  • Order matters for humans: keep original order, avoid sorting.
  • Order matters for diffs/tests: sort keys.
  • Order matters for display: keep original order but make a note in docs.

ensure_ascii for internationalization

When ensureascii=True (the default), non-ASCII characters become escape codes. It’s valid JSON, but it hides the data. I treat ensureascii=False as a best practice unless I’m outputting to a strict ASCII-only system.

New: production guardrails and conventions

In larger codebases, consistency is everything. Here are a few guardrails I’ve adopted to keep JSON output safe and predictable.

1) Centralize formatting policy

I keep a single formatter in a shared module. That ensures every team member and script uses the same rules.

import json

DEFAULTJSONOPTIONS = {

"indent": 2,

"sort_keys": True,

"ensure_ascii": False,

}

def format_json(obj, overrides):

opts = {DEFAULTJSONOPTIONS, overrides}

return json.dumps(obj, opts)

This makes it trivial to change indentation or sorting across the entire project.

2) Separate human output from machine output

If an API endpoint serves machines, I don’t pretty print. If a file is meant for humans, I do. Mixing the two makes systems brittle.

3) Explicit flags for debug output

I use a debug flag rather than relying on ad hoc decisions. That gives me a controlled way to enable heavy formatting when needed.

New: parsing pitfalls and how to spot them quickly

I’ve seen plenty of subtle mistakes when people handle JSON in a hurry. These quick checks save time:

Check 1: Is it already a dict/list?

If your pretty print looks like a quoted blob, you probably printed a JSON string rather than parsed data.

import json

raw = ‘{"a": 1}‘

obj = json.loads(raw)

print(json.dumps(obj, indent=2)) # correct

print(json.dumps(raw, indent=2)) # incorrect, prints escaped string

Check 2: Are numbers actually numbers?

If you see quotes around your numbers, they’re strings. That’s almost always unintentional.

Check 3: Are booleans Python or JSON?

Python uses True/False, JSON uses true/false. If you’re manually building strings, you can accidentally mix these, which breaks strict JSON parsers.

New: pretty printing with custom transforms

Pretty printing is often combined with normalization. Here’s a workflow I use when cleaning up messy JSON:

import json

from datetime import datetime

raw = {

"timestamp": "2026-01-26 15:43:00",

"user_id": "42",

"active": "true",

}

Normalize types

normalized = {

"timestamp": datetime.fromisoformat(raw["timestamp"]).isoformat(),

"userid": int(raw["userid"]),

"active": raw["active"].lower() == "true",

}

print(json.dumps(normalized, indent=2, sort_keys=True))

This keeps the output clean and makes sure the JSON types are correct. I do this before writing files that will be used in tests or audits.

New: alternative approaches you might consider

The built-in json module is usually enough, but sometimes you want different trade-offs.

Option 1: Pretty print with pprint for Python objects

If you’re not committed to JSON output and you just want readability in the terminal, pprint can be faster for quick checks.

from pprint import pprint

pprint({"a": [1, 2, 3], "b": {"x": 1, "y": 2}})

This isn’t JSON, but it’s easy to read during debugging.

Option 2: Use a validation-first approach

If you’re dealing with strict JSON consumers, you might want to validate before pretty printing. That’s especially useful for data pipelines where invalid JSON should fail fast.

Option 3: Use a schema-aware formatter

In data-heavy systems, a schema-aware formatter can reorder or group fields meaningfully. That’s outside the scope of basic pretty printing, but I’ve seen teams use schemas to keep JSON consistent across services.

New: monitoring and safety in long-running systems

Pretty printing might seem like a local concern, but it can have operational impact. Here’s how I keep it safe:

  • Guard output size: Don’t log huge payloads even if they’re pretty. Use truncation or sampling.
  • Limit depth: If possible, limit how deep you print nested structures.
  • Avoid secrets: Pretty output can expose secrets more clearly, so always sanitize sensitive fields.

I once saw a team debug by logging an entire auth payload in a pretty format, which made the secret keys visible in plain text. The readable output actually made the mistake easier to spot—but also more dangerous. Now I always mask fields like token, password, and secret before printing.

New: a masking helper that keeps pretty output safe

Here’s a simple helper to sanitize sensitive keys before printing:

import json

SENSITIVEKEYS = {"password", "token", "secret", "apikey"}

def mask_sensitive(obj):

if isinstance(obj, dict):

return {

k: ("*" if k.lower() in SENSITIVEKEYS else masksensitive(v))

for k, v in obj.items()

}

if isinstance(obj, list):

return [mask_sensitive(v) for v in obj]

return obj

payload = {"user": "ana", "token": "abc123", "profile": {"api_key": "xyz"}}

safe = mask_sensitive(payload)

print(json.dumps(safe, indent=2, ensure_ascii=False))

This is a small step, but it prevents easy leaks in logs and screenshots.

New: working with JSON Lines (NDJSON)

Sometimes data comes as newline-delimited JSON, where each line is a separate JSON object. Pretty printing line-by-line doesn’t make much sense, but you can pretty print a sample or transform it into a standard JSON array for review.

import json

sample = []

with open("events.ndjson", "r", encoding="utf-8") as f:

for i, line in enumerate(f):

if i >= 3:

break

sample.append(json.loads(line))

print(json.dumps(sample, indent=2, ensure_ascii=False))

This gives you a readable snapshot without changing the file format.

New: how I decide indentation in different contexts

People often ask if there’s a “correct” indent. I treat it like typography—choose what matches the medium:

  • Logs: indent=2 or no indent
  • Docs/configs: indent=2 or indent=4
  • Snapshots/tests: indent=2 plus sort_keys=True
  • API responses: usually no indent in production, maybe indent behind a debug flag

Consistency matters more than the exact number.

New: troubleshooting checklist

When pretty printing doesn’t work as expected, I walk through this short checklist:

1) Is the data already parsed (dict/list), or is it a JSON string?

2) Are you using the right encoder for datetimes and decimals?

3) Are you accidentally sorting keys and losing a meaningful order?

4) Are you printing too much data for your terminal?

5) Are you sure the output is valid JSON (no NaN, no cycles)?

This catches most issues in minutes.

New: a full mini-utility script I actually use

When I need a quick, reusable command-line tool for prettifying JSON, I keep a tiny script like this:

import json

import sys

from datetime import datetime, date

from decimal import Decimal

class SafeJSONEncoder(json.JSONEncoder):

def default(self, obj):

if isinstance(obj, (datetime, date)):

return obj.isoformat()

if isinstance(obj, Decimal):

return str(obj)

return super().default(obj)

Read from stdin and output pretty JSON

try:

data = json.load(sys.stdin)

print(json.dumps(data, indent=2, ensureascii=False, sortkeys=True, cls=SafeJSONEncoder))

except json.JSONDecodeError as e:

print(f"Invalid JSON: {e}", file=sys.stderr)

sys.exit(1)

It’s minimal, but it handles most of the messy cases I run into without extra dependencies.

New: collaborating with teammates and AI assistants

In 2026 workflows, I often use AI to draft transforms or extraction logic, then I pretty print the output before committing it. It’s like a “sanity check” for machine-generated code. I keep a rule: if I wouldn’t show the output to a human reviewer, I probably shouldn’t trust it in production.

I also share pretty-printed samples in code reviews. A clean diff is a huge time saver. If I see compressed JSON in a PR, I’ll often ask for a pretty-printed version before approving the change.

New: keeping pretty output consistent across languages

Many teams have services in multiple languages. If Python pretty prints with 2 spaces and another service uses 4, diffs become messy. I recommend defining a cross-language JSON style guide:

  • Indentation: 2 spaces
  • Key sorting: yes for tests, no for APIs
  • Unicode: preserve
  • Float compliance: allow_nan=False for exports

Once you define this, Python is easy to align with other tools.

New: security and privacy notes I never skip

Pretty output makes data easier to read, and that includes sensitive data. I always ask:

  • Are we printing secrets in logs or console output?
  • Are we writing PII to disk in a way that’s easy to access?
  • Are we exposing hidden fields to users who shouldn’t see them?

Pretty printing should never bypass your masking and redaction rules. If it does, you’ve got a risk.

Key takeaways and next steps

Pretty printing is the easiest way to improve the readability of JSON in Python, and the built-in json module already gives you everything you need. When you add consistent indentation, stable ordering, and safe encoding for real-world types like datetimes and Decimals, your output becomes reliable, test-friendly, and human-readable. That saves time during debugging, reduces mistakes in reviews, and makes logs and exports safer to work with.

If you want to put this into action quickly, I’d start with a small helper like pretty_json(obj) and make it the default in your debugging and test flows. Then add a custom encoder if you touch dates, money, or file paths. Finally, set clear rules for when to avoid pretty printing so you don’t accidentally slow down production systems.

Pretty printing isn’t just formatting—it’s a clarity tool. When you use it consistently, it becomes a low-friction habit that protects you from subtle bugs and painful investigations later.

Expansion Strategy

Add new sections or deepen existing ones with:

  • Deeper code examples: More complete, real-world implementations
  • Edge cases: What breaks and how to handle it
  • Practical scenarios: When to use vs when NOT to use
  • Performance considerations: Before/after comparisons (use ranges, not exact numbers)
  • Common pitfalls: Mistakes developers make and how to avoid them
  • Alternative approaches: Different ways to solve the same problem

If Relevant to Topic

  • Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
  • Comparison tables for Traditional vs Modern approaches
  • Production considerations: deployment, monitoring, scaling
Scroll to Top