Pretty print JSON with json.dumps
import json
reading = {
"station": "São Paulo-Mirante",
"temp_c": 24.5,
"alerts": ["heat", "uv"],
"calibrated": True,
"battery": None,
}
print(json.dumps(reading, indent=4))
{
"station": "S\u00e3o Paulo-Mirante",
"temp_c": 24.5,
"alerts": [
"heat",
"uv"
],
"calibrated": true,
"battery": null
}
True and None become true and null, and non-ASCII characters are escaped (S\u00e3o) unless you pass ensure_ascii=False.
Arguments that change the output
| Argument | Effect |
|---|---|
indent=2 |
Spaces per level. "\t" indents with tabs; 0 adds line breaks but no indent. |
sort_keys=True |
Sorts the keys of every object, nested ones included. |
ensure_ascii=False |
Writes ã, é and emoji as they are instead of \u escapes. |
separators=(",", ":") |
Removes the spaces after commas and colons. With no indent, gives the smallest output. |
default=str |
Converts values json cannot handle, such as datetime and Decimal. |
print(json.dumps(reading, indent=2, sort_keys=True, ensure_ascii=False))
{
"alerts": [
"heat",
"uv"
],
"battery": null,
"calibrated": true,
"station": "São Paulo-Mirante",
"temp_c": 24.5
}
JSON Prettier does the same in the browser, with an Escape non-ASCII switch that matches ensure_ascii.
Write it to a file
with open("reading.json", "w", encoding="utf-8") as f:
json.dump(reading, f, indent=2, ensure_ascii=False)
f.write("\n")
json.dump writes to a file object and adds no final newline. Pass encoding="utf-8": before Python 3.15, Windows defaults to a legacy code page, and characters outside it raise UnicodeEncodeError.
To reformat a file in place, read it in one with block and write it in a second. Opening with "w" empties the file at once, so it must come after json.load:
with open("export.json", encoding="utf-8") as f:
data = json.load(f)
with open("export.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
json.dump(data, sys.stdout, indent=2) prints in chunks, without building one large string the way print(json.dumps(...)) does.
From the terminal: json.tool
python -m json.tool reading.json
python -m json.tool --indent 2 --sort-keys reading.json
python -m json.tool raw.json pretty.json
curl -s https://api.github.com/repos/python/cpython | python -m json.tool
The third line writes to pretty.json. Python 3.14 added python -m json as the preferred spelling and colors the output in a terminal; json.tool still works. Use python3 on macOS and Linux, and py on Windows if python is not found.
| Flag | Effect |
|---|---|
--indent N |
N spaces per level; the default is 4 |
--tab |
Indent with tabs |
--sort-keys |
Sort object keys |
--no-ensure-ascii |
Keep non-ASCII characters (3.9+) |
--compact |
One line, no spaces (3.9+) |
--json-lines |
One document per input line (3.8+) |
Floats are rewritten on the way through: {"price": 19.90, "qty": 1e2} prints as 19.9 and 100.0. Integers of any size survive, and jq 1.7 and later keep 19.90 as written. Invalid input prints the parser’s message and exits with status 1:
$ echo '{"temp_c": 24.5,}' | python -m json
Illegal trailing comma before end of object: line 1 column 16 (char 15)
For jq, Node.js and PowerShell, see pretty print JSON on the command line.
pprint is not JSON
from pprint import pprint
pprint(reading)
{'alerts': ['heat', 'uv'],
'battery': None,
'calibrated': True,
'station': 'São Paulo-Mirante',
'temp_c': 24.5}
Single quotes, True and None make this invalid JSON, and keys are sorted unless you pass sort_dicts=False or call pprint.pp. Use it for debugging, and json.dumps for anything that must be JSON.
Pretty print an API response
import json
import requests
r = requests.get("https://api.github.com/repos/python/cpython", timeout=10)
r.raise_for_status()
print(json.dumps(r.json(), indent=2, ensure_ascii=False))
Don’t pass r.text to json.dumps. It is already a string, so you get one quoted line full of backslashes:
raw = '{"station": "Mirante", "temp_c": 24.5}'
print(json.dumps(raw, indent=2))
"{\"station\": \"Mirante\", \"temp_c\": 24.5}"
Parse it with json.loads(raw) first. The same goes for JSON inside a string field, common in webhook and queue payloads:
event = {"type": "invoice.paid", "body": "{\"invoice\": \"INV-88\", \"amount\": 120}"}
event["body"] = json.loads(event["body"])
print(json.dumps(event, indent=2))
If .json() fails with Expecting value: line 1 column 1 (char 0), the server sent HTML or an empty body: see JSONDecodeError: Expecting value.
JSON Lines logs
.jsonl (NDJSON) files hold one document per line, so json.load fails on them. Format line by line:
with open("events.jsonl", encoding="utf-8") as f:
for line in f:
if line.strip():
print(json.dumps(json.loads(line), indent=2))
The if skips blank lines. python -m json.tool --json-lines stops at the first one with Expecting value: line 2 column 1 (char 1).
Values json cannot write
TypeError: Object of type datetime is not JSON serializable
when serializing dict item 'taken_at'
datetime, Decimal, set and your own classes raise this; Python 3.14 adds the second line, which names the key. Pass default=str, or default=lambda o: o.isoformat() when the unknown values are dates.
Keys must be strings. int, float, bool and None keys are converted ({7: "x"} becomes {"7": "x"}), and anything else fails with TypeError: keys must be str, int, float, bool or None, not tuple.
float("nan") and float("inf") are written as NaN and Infinity, which are not valid JSON and make a browser’s JSON.parse throw. allow_nan=False raises ValueError: Out of range float values are not JSON compliant instead. For the browser side, see pretty print JSON in JavaScript; every other guide is in JSON guides.