print(json.dumps(data, indent=2)) is the answer. Two flags turn it from adequate into correct: ensure_ascii=False, without which Køpenhavn is written as K\u00f8penhavn, and default=str, without which a datetime anywhere in your data raises TypeError: Object of type datetime is not JSON serializable. On the command line, python -m json.tool does the same job with no script at all.
It is a small function with a few sharp edges, and the edges are exactly where people end up searching.
Table of contents
- The basics
- The trailing-space detail
- Unicode: the flag you want almost always
- Objects that will not serialise
- From the command line
- pprint, and printing large structures
- How this fits the rest of the stack
- FAQ
The basics
import json
data = {"name": "Ada", "roles": ["admin", "dev"], "active": True}
print(json.dumps(data, indent=2))
# Sorted keys -- essential for diffing two outputs
print(json.dumps(data, indent=2, sort_keys=True))
# A JSON string rather than a dict: parse first
raw = '{"b": 2, "a": 1}'
print(json.dumps(json.loads(raw), indent=2, sort_keys=True))
indent takes an integer or a string, so indent='\t' gives tabs. indent=0 puts each item on its own line with no leading space; indent=None, the default, produces one line.
sort_keys=True matters more than it looks. Dictionaries preserve insertion order, so two structurally identical objects built differently produce different output and a meaningless diff. Sorting makes the comparison real.
The trailing-space detail
By default json.dumps separates items with ', ' and keys from values with ': '. With indent set, the comma is followed by a newline, leaving a trailing space on every line.
# Some linters and diff tools object to the trailing whitespace
print(json.dumps(data, indent=2, separators=(',', ': ')))
# Most compact possible output -- for network payloads, not humans
print(json.dumps(data, separators=(',', ':')))
Python 3 already omits the trailing space when indent is not None, so this is mostly a concern in older code. The compact form is worth knowing separately: it strips every optional space, which is what you want when the JSON is going over the wire rather than to a human.
Unicode: the flag you want almost always
data = {"city": "København", "note": "café"}
print(json.dumps(data, indent=2))
# {
# "city": "K\u00f8benhavn",
# "note": "caf\u00e9"
# }
print(json.dumps(data, indent=2, ensure_ascii=False))
# {
# "city": "København",
# "note": "café"
# }
The default exists because escaped ASCII is safe to transmit through anything, including systems with confused encoding. Both forms are valid JSON and parse to the same string.
For anything a human reads, ensure_ascii=False is what you want. When writing to a file, specify the encoding explicitly or Windows will use the system code page and raise UnicodeEncodeError:
with open('out.json', 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
Note json.dump with no s writes to a file object; json.dumps returns a string. Mixing them up produces a confusing TypeError about the first argument.
Objects that will not serialise
The most common runtime failure. datetime, Decimal, UUID, set, and dataclasses all raise TypeError.
from datetime import datetime, timezone
from decimal import Decimal
from uuid import uuid4
import json
data = {"id": uuid4(), "at": datetime.now(timezone.utc), "amount": Decimal("9.99")}
# json.dumps(data) -> TypeError
# Quick fix: stringify anything unknown
print(json.dumps(data, indent=2, default=str))
# Controlled fix: decide per type
def encode(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Decimal):
return float(obj) # or str(obj) to keep precision
if isinstance(obj, set):
return sorted(obj)
raise TypeError(f"not serialisable: {type(obj).__name__}")
print(json.dumps(data, indent=2, default=encode))
default=str is right for a debug print and wrong for an API response, because it silently produces whatever str() gives — a Decimal becomes "9.99" as a string, and a consumer expecting a number breaks.
The Decimal case is a genuine decision, not an oversight. float loses precision, which matters for money; str keeps it but changes the JSON type. Pick deliberately and be consistent, because the choice leaks into every consumer of the API.
From the command line
# Format a file in place-ish
python -m json.tool messy.json pretty.json
# From a pipe
curl -s https://api.example.com/users | python -m json.tool
# Sorted, and without escaping non-ASCII
curl -s https://api.example.com/users \
| python -m json.tool --sort-keys --no-ensure-ascii
# Validate without printing
python -m json.tool config.json > /dev/null && echo valid
json.tool needs no dependencies, which makes it the reliable choice on a server where jq is not installed. jq is better when you also want to filter or transform; json.tool is better when you just want it readable.
That last line is a useful pre-commit or CI check — it exits non-zero on invalid JSON, so a malformed config file fails the build rather than the deploy.
pprint, and printing large structures
from pprint import pprint
# Python repr, not JSON -- single quotes, True instead of true
pprint(data, width=100, sort_dicts=False)
# Truncate a deep structure
pprint(data, depth=2)
pprint output is not JSON and cannot be parsed as JSON — it uses Python literals. It is better for inspecting objects in a REPL; json.dumps is better for anything another program will read.
For very large payloads, printing the whole thing is rarely what you want. Truncate to a sample, or reach for a structured logger that keeps the JSON as fields rather than embedding a formatted blob in a message string.
That last point matters in production. Pretty-printed JSON in a log is multi-line, so log collectors treat each line as a separate entry and the structure is lost. Log compact JSON as one line per event, and pretty-print only when a human is looking at it. Runtime logs that keep the request and the JSON payload in one entry — the shape you get from per-deploy logs on RunxBuild — are far easier to search than a formatted blob split across twenty lines.
How this fits the rest of the stack
json.dumps(data, indent=2) for readability, sort_keys=True when you need to diff, ensure_ascii=False for anything a human reads, and default=str or a custom encoder for datetimes and Decimals. json.dump writes to a file, json.dumps returns a string, and specify encoding='utf-8' when writing.
On a server, python -m json.tool is always available. Keep production logs compact and single-line — pretty-printing is for humans, not log collectors. If you are working out what the service producing that JSON costs to run, the RunxBuild hosting calculator itemises service, database, storage, and bandwidth separately.
Useful related references:
- Python Print to stderr: And Why Logging Is Usually Better
- Python str: How Objects Print, and Why You Probably Want repr Instead
- json.loads in Python: loads vs load, and the Errors You Will Actually Hit
- Python services on RunxBuild
FAQ
How do I pretty print JSON in Python?
print(json.dumps(data, indent=2)). If you are starting from a JSON string rather than a dict, parse it first with json.loads. Add sort_keys=True when you need to compare two outputs, since dictionaries preserve insertion order and would otherwise produce a noisy diff.
Why does json.dumps escape accented characters?
ensure_ascii defaults to True, so non-ASCII characters are written as \uXXXX escapes. That is valid JSON and safe through any transport, but unreadable. Pass ensure_ascii=False for human-facing output, and open files with encoding='utf-8' so writing does not fail on Windows.
How do I fix ‘Object of type datetime is not JSON serializable’?
Pass a default function that converts unknown types. default=str is fine for debugging; for anything structured, write a function that returns obj.isoformat() for datetimes and makes a deliberate choice for Decimal — float loses precision and str changes the JSON type.
How do I pretty print JSON from the command line with Python?
Pipe it to python -m json.tool, which needs no extra packages. Add --sort-keys and --no-ensure-ascii as needed. Redirecting the output to /dev/null and checking the exit code makes it a quick JSON validity check for CI.
What is the difference between json.dumps and pprint?
json.dumps produces valid JSON that other programs can parse. pprint produces Python repr output with single quotes and True rather than true, which is not JSON. Use pprint for inspecting objects in a REPL and json.dumps for anything that leaves your process.