DEV Community

Dan Lumatic
Dan Lumatic

Posted on Originally published at demo.lumatic-labs.com AI-assisted

Pretty print JSON in Python: json.dumps, json.tool, and the three errors you will hit

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. Add sort_keys=True and two dumps become diffable.
  • No code at all: python -m json.tool data.json formats from the terminal, reads stdin, and fails loudly on invalid JSON.
  • datetime raises not JSON serializable: default=str for a quick look, isoformat() for output other programs parse.
  • café becomes caf\u00e9: ensure_ascii=False, plus encoding="utf-8" on the open() when you write a file.
  • JSONDecodeError on 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))
Enter fullscreen mode Exit fullscreen mode
{
  "active": true,
  "manager": null,
  "name": "Ada",
  "score": 59.0,
  "tags": [
    "a",
    "b",
    "c"
  ]
}
Enter fullscreen mode Exit fullscreen mode

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:

  • True prints as true, and None prints as null.
  • Floats keep Python's form, so 59.0 stays 59.0, not 59.
  • sort_keys=True orders 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))
Enter fullscreen mode Exit fullscreen mode

Write it back formatted:

with open("data.json", "w") as f:
    json.dump(data, f, indent=2)
Enter fullscreen mode Exit fullscreen mode

From the terminal, no script:

python -m json.tool data.json
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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}'
Enter fullscreen mode Exit fullscreen mode

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=(",", ": "))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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é"}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Try this: when a script chokes on a file, run python -m json.tool file.json before 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), with ensure_ascii=False and encoding="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 separators too

Top comments (0)