You printed an API response and got one line, 400 characters wide, with the field you need somewhere in the middle. The fix is one call, print(json.dumps(data, indent=2)). What follows is what happens on real data.
If what you printed was the dict itself, the problem is different. print(data) shows Python's repr: single quotes, True, None, like {'a': 1, 'b': [1, 2]}. JSON parsers reject that text. json.dumps is what produces JSON.
Three errors show up on the first real dataset: a datetime that raises TypeError: Object of type datetime is not JSON serializable, café printed as caf\u00e9, and a file that dies on JSONDecodeError. Each has a quick fix. Two of the popular quick fixes fail later: default=str writes dates other programs have to guess at, and ensure_ascii=False writing to a file needs an explicit encoding or the text can arrive mangled. Below: the calls, the flags worth knowing, and where each fix is safe.
TL;DR
print(json.dumps(data, indent=2))pretty prints any dict or list. Addsort_keys=Trueand two dumps become diffable.- No code at all:
python -m json.tool data.jsonformats from the terminal, reads stdin, and fails loudly on invalid JSON.datetimeraisesnot JSON serializable:default=strfor a quick look,isoformat()for output other programs parse.cafébecomescaf\u00e9:ensure_ascii=False, plusencoding="utf-8"on theopen()when you write a file.JSONDecodeErroron load: the message names the line and column. Trailing commas and single quotes are the usual causes.
The one-liner: indent expands every list and object
indent is the flag that does the formatting. Without it, json.dumps returns one compact line. With it, every object and list spreads over indented lines:
import json
data = {
"name": "Ada",
"active": True,
"score": 59.0,
"tags": ["a", "b", "c"],
"manager": None,
}
print(json.dumps(data))
# {"name": "Ada", "active": true, "score": 59.0, "tags": ["a", "b", "c"], "manager": null}
print(json.dumps(data, indent=2, sort_keys=True))
{
"active": true,
"manager": null,
"name": "Ada",
"score": 59.0,
"tags": [
"a",
"b",
"c"
]
}
indent expands lists as well as objects: every list item gets its own line.
Three details in that output are worth knowing, and all of them are valid JSON:
-
Trueprints astrue, andNoneprints asnull. - Floats keep Python's form, so
59.0stays59.0, not59. -
sort_keys=Trueorders keys alphabetically. Use it whenever you plan to diff two dumps.
One note if you were about to reach for pprint instead: it prints Python syntax ('city': 'café', True, None), and a JSON parser rejects that too. json.dumps is the call when the output needs to be JSON.
Pretty print a JSON file, a string, or an API response
json.dump (no "s") writes to a file object, json.dumps returns a string, and python -m json.tool needs no code at all. The module docs list both signatures: json, JSON encoder and decoder.
Read a file and print it:
import json
with open("data.json") as f:
data = json.load(f)
print(json.dumps(data, indent=2))
Write it back formatted:
with open("data.json", "w") as f:
json.dump(data, f, indent=2)
From the terminal, no script:
python -m json.tool data.json
It reads stdin too:
Try this: pipe any HTTP response through it.
curl -s https://api.example.com/data | python -m json.tool
And if the JSON is already a string in a variable or your REPL, parse it, then dump it again:
pretty = json.dumps(json.loads(blob), indent=2)
One catch: json.tool indents with 4 spaces, indent=2 with 2. Both are valid, but a file formatted one way next to a file formatted the other differs on every indented line. Pick one value and use it everywhere: indent=2 is the common choice, 4 makes sense when the JSON sits next to Python source that indents with 4.
Pin indent and separators when files get compared
A snapshot test starts failing, or a git diff marks every line of a JSON file as changed, and the data underneath is identical. Check what wrote the file. json.dumps defaults to the separators (', ', ': '), a comma plus a space. Set indent and the item separator silently becomes (',', ': '), because the newline now carries the spacing. Same dict, both ways:
d = {"a": 1, "b": [1, 2]}
json.dumps(d)
# '{"a": 1, "b": [1, 2]}'
json.dumps(d, indent=2)
# '{\n "a": 1,\n "b": [\n 1,\n 2\n ]\n}'
The compact form has a space after each comma, the pretty form has none. So two files with identical data can differ in their bytes, and a test that compares them byte for byte fails even though json.loads returns equal data from both.
If a test, a checksum or a diff compares your formatted JSON, pass indent and separators explicitly in every writer that touches the file:
json.dump(data, f, indent=2, separators=(",", ": "))
The three errors you will actually hit
One of the three is a TypeError from your own data. One is valid JSON that only looks wrong. The third means the input is not valid JSON.
1. TypeError: Object of type datetime is not JSON serializable
JSON has no date type, so json.dumps refuses to guess:
from datetime import datetime
event = {"created": datetime(2026, 10, 11, 20, 0)}
json.dumps(event)
# TypeError: Object of type datetime is not JSON serializable
The quick fix for a debugging session is default=str, which calls str() on anything JSON can't handle:
print(json.dumps(event, default=str))
# {"created": "2026-10-11 20:00:00"}
The same traceback fires for set, Decimal and UUID objects, and default=str handles all of them: a set comes out as "{1, 2}", a Decimal as "1.1", a UUID as its canonical string. That stops the traceback for any type JSON does not know. It does not make the values good: the set came out as the string "{1, 2}", which no parser reads back as a set.
But look at what it wrote for the datetime: a space in the middle and no timezone, so another program has to guess the format. For output anyone else will parse, convert the value yourself with isoformat(), which produces the ISO 8601 form:
event = {"created": datetime(2026, 10, 11, 20, 0).isoformat()}
print(json.dumps(event))
# {"created": "2026-10-11T20:00:00"}
2. Non-ASCII text shows up as \u00e9
json.dumps escapes non-ASCII characters by default. café becomes caf\u00e9, and both forms are valid JSON that decode to the same string.
print(json.dumps({"city": "café"}))
# {"city": "caf\u00e9"}
print(json.dumps({"city": "café"}, ensure_ascii=False))
# {"city": "café"}
Add ensure_ascii=False when a human will read the output. If that output goes to a file, also open the file with an explicit encoding:
with open("data.json", "w", encoding="utf-8") as f:
json.dump(data, f, indent=2, ensure_ascii=False)
The default encoding for open() is UTF-8 only where Python's UTF-8 Mode is enabled; otherwise it follows the locale, and non-ASCII written without an explicit encoding can fail or arrive mangled. Passing encoding="utf-8" makes the result the same everywhere. Details in the io module docs, Text encoding.
3. JSONDecodeError when you load
json.loads is stricter than most people expect. This looks harmless:
json.loads('{"a": 1,}')
# json.decoder.JSONDecodeError: Illegal trailing comma before end of object: line 1 column 8 (char 7)
The message names the position, so start there. The usual suspects:
- A trailing comma, as above.
- Single quotes. JSON strings use double quotes only.
- Pasted text that was never JSON, such as a log prefix in front of the opening brace.
json.tool reports the same problem at the command line, and exits with status 1:
echo '{"a": 1,}' | python -m json.tool
# Illegal trailing comma before end of object: line 1 column 8 (char 7)
Try this: when a script chokes on a file, run
python -m json.tool file.jsonbefore you touch your code. If the tool fails, the file is the problem. If it prints cleanly, the bug is in your script.
Which call for which job
Everything above, sorted by where the data sits:
- Dict or list in front of you:
print(json.dumps(data, indent=2)) - JSON already a string in a variable:
json.dumps(json.loads(blob), indent=2) - Writing a file:
json.dump(data, f, indent=2), withensure_ascii=Falseandencoding="utf-8"when non-ASCII is involved - No code:
python -m json.tool data.json, or pipe curl output through it - Files that get diffed or checksummed: pin
separatorstoo
Top comments (0)