CoreMesh

Python JSON Examples: dumps, loads, Custom Types, Errors

Runnable Python json examples: dumps vs dump, loads vs load, pretty printing, datetime and custom types, plus the errors you'll actually hit.

Python's built-in json module turns Python objects into JSON text with dumps() and dump(), and turns JSON text back into Python objects with loads() and load(). The functions ending in s work on strings. The ones without it work on file objects.

That's the whole core. The rest of this page is working examples: pretty printing, sorting keys, datetime and other custom types, and the errors that trip people up. Everything here uses the standard library only, so there's nothing to install.

Python json module examples: the four functions#

The mnemonic: the trailing s means "string". Without it, you hand the function a file-like object.

Function Input Output
json.dumps(obj) Python object JSON str
json.dump(obj, fp) Python object, writable file writes JSON to the file
json.loads(s) str, bytes or bytearray Python object
json.load(fp) readable file (text or binary) Python object

Here's the smallest round trip:

python
import json

user = {"name": "Ada", "langs": ["python", "c"], "active": True, "manager": None}

text = json.dumps(user)
print(text)
print(type(text))

back = json.loads(text)
print(back)
print(back == user)

Notice what happened to the types: True became true, None became null, and back again on the way in. JSON arrays come back as Python lists and JSON objects come back as dicts.

dumps vs dump (and loads vs load)#

Use dumps() when you want the JSON as a string, for example to send in an HTTP body or log line. Use dump() when you want it written straight to a file or any object with a .write() method that accepts str. The module always produces str, never bytes, so a file opened in binary mode won't work with dump().

python
import json

data = {"id": 7, "tags": ["a", "b"]}

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

with open("data.json", encoding="utf-8") as f:
    loaded = json.load(f)

On the reading side, loads() accepts str, bytes or bytearray. load() accepts a text or binary file. For bytes, the encoding should be UTF-8, UTF-16 or UTF-32.

You can test the file-style functions without touching disk by using io.StringIO:

python
import json
from io import StringIO

buf = StringIO()
json.dump(["streaming API"], buf)
print(buf.getvalue())      # ["streaming API"]

print(json.load(StringIO('["streaming API"]')))

One gotcha with dump(): it isn't a framed protocol. If you call it several times on the same file, the result is not a valid JSON file. Write one object per file, or wrap your records in a single list. If you want one record per line, write each dumps() result plus a newline yourself.

Pretty printing and compact output#

By default, dumps() returns one line. Pass indent to get readable output and sort_keys=True for stable key order.

python
import json

doc = {"6": 7, "4": 5, "nested": {"z": [1, 2], "a": None}}

print(json.dumps(doc, indent=4, sort_keys=True))
print(json.dumps(doc, indent="\t"))
print(json.dumps(doc, separators=(",", ":")))

A few details from the docs:

  • A positive integer indent means that many spaces per level. A string such as "\t" is used for each level instead.
  • If indent is 0, negative, or "", you only get newlines.
  • The default separators are (', ', ': ') without indent, and (',', ': ') with it.
  • For the smallest output, use separators=(',', ':').

The module keeps insertion order by default, so sort_keys is only needed when you want sorted output, for example to get clean diffs.

Pretty printing from the shell#

The module doubles as a validator and formatter:

bash
echo '{"json": "obj"}' | python -m json
python -m json input.json output.json
python -m json --sort-keys --indent 2 input.json

Since Python 3.14 you can run python -m json directly. python -m json.tool still works for backward compatibility. Other options include --tab, --no-indent, --compact, --no-ensure-ascii and --json-lines, which parses every input line as a separate JSON object. The indent-related options are mutually exclusive.

Python json datetime and custom types#

The encoder only knows dict, list, tuple, str, int, float, True, False and None. Hand it a datetime and you get a TypeError. There are two ways to fix that.

The quick way is the default argument: a function called for anything the encoder can't handle. It should return something encodable or raise TypeError.

python
import json
from datetime import datetime, timezone
from decimal import Decimal

def encode_extra(obj):
    if isinstance(obj, datetime):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return str(obj)
    raise TypeError(f"Cannot serialize object of {type(obj)}")

event = {
    "at": datetime(2026, 10, 14, 9, 30, tzinfo=timezone.utc),
    "price": Decimal("19.99"),
}
print(json.dumps(event, default=encode_extra, indent=2))

I'd store datetimes as ISO 8601 strings. They sort correctly as text and every language can parse them. I'd store Decimal as a string too, because many JSON consumers read numbers as IEEE 754 doubles and the docs warn about range and precision limits there.

The reusable way is subclassing JSONEncoder and overriding default(), then passing cls=:

python
import json

class ComplexEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, complex):
            return [obj.real, obj.imag]
        # Let the base class raise the TypeError
        return super().default(obj)

print(json.dumps(2 + 1j, cls=ComplexEncoder))   # [2.0, 1.0]

Decoding back into real objects#

Going the other way, object_hook receives every decoded JSON object (as a dict) and returns whatever you want instead. A common pattern is tagging custom types on the way out and spotting the tag on the way in:

python
import json
import decimal

def as_complex(dct):
    if "__complex__" in dct:
        return complex(dct["real"], dct["imag"])
    return dct

def custom_json(obj):
    if isinstance(obj, complex):
        return {"__complex__": True, "real": obj.real, "imag": obj.imag}
    raise TypeError(f"Cannot serialize object of {type(obj)}")

text = json.dumps({"z": 1 + 2j}, default=custom_json)
print(text)
print(json.loads(text, object_hook=as_complex))

# Floats as Decimal, no binary rounding on the way in
print(json.loads("1.1", parse_float=decimal.Decimal))

Related hooks: parse_float and parse_int take the number's string, and object_pairs_hook gets an ordered list of pairs (and wins over object_hook if both are set). array_hook is new in Python 3.15.

Datetimes don't have a built-in decoder. You decode them yourself, for example by calling datetime.fromisoformat() on the fields you know are dates, after loads(). That's usually clearer than guessing from string shape in a hook.

Common errors and gotchas#

TypeError: Object of type X is not JSON serializable. You passed something outside the basic types. Add a default= function or a cls= encoder, as above.

JSONDecodeError. The input isn't valid JSON. The message includes the position. For example, {1.2:3.4} fails with Expecting property name enclosed in double quotes: line 1 column 2 (char 1). The usual causes are single quotes, trailing commas, and Python reprs (True, None) pasted in place of real JSON.

python
import json

bad_inputs = ['{"a": 1,}', "{'a': 1}", '{"a": }', ""]

for s in bad_inputs:
    try:
        json.loads(s)
    except json.JSONDecodeError as e:
        print(repr(s), "->", e.msg, "| line", e.lineno, "col", e.colno)

Keys always become strings. JSON object keys are always str, so loads(dumps(x)) != x when x has non-string keys:

python
import json

x = {1: "one", 2: "two"}
print(json.dumps(x))
print(json.loads(json.dumps(x)))
print(json.loads(json.dumps(x)) == x)

# tuples come back as lists
print(json.loads(json.dumps((1, 2))))

Note that sort_keys sorts before conversion, so numeric keys sort by value, not by their string form.

Non-ASCII output is escaped. With the default ensure_ascii=True, json.dumps('é') gives "\u00e9". Pass ensure_ascii=False to keep characters as they are. Then make sure you open the file with an explicit encoding such as utf-8.

NaN and Infinity. By default the module writes NaN, Infinity and -Infinity, which are not valid JSON per the RFC. Pass allow_nan=False to dumps() to get a ValueError instead, or use parse_constant when loading to reject them.

Circular references. check_circular=True is the default, so a self-referencing list raises an error rather than recursing forever. Turning the check off can end in a RecursionError (or worse).

Untrusted input. The docs warn that a malicious JSON string can consume considerable CPU and memory, and recommend limiting the size of data you parse. Check the length of request bodies before calling loads(). Since Python 3.11, the default integer parsing also limits the length of integer strings to help avoid denial of service.

Keyword-only arguments. Since Python 3.6, everything after the first argument (or after fp) must be passed by keyword. json.dumps(data, 4) fails; write json.dumps(data, indent=4).

Beginner sample: read, change, write#

Most real scripts do this: load a file, edit it, write it back. This version runs end to end using a file in the current directory.

python
import json

config = {"name": "demo", "retries": 3, "hosts": ["a.example", "b.example"]}

with open("sample_config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=2)

with open("sample_config.json", encoding="utf-8") as f:
    cfg = json.load(f)

cfg["retries"] += 1
cfg["hosts"].append("c.example")

with open("sample_config.json", "w", encoding="utf-8") as f:
    json.dump(cfg, f, indent=2)

with open("sample_config.json", encoding="utf-8") as f:
    print(f.read())

If you want a PDF version of a tutorial like this, the official json module documentation is the reference to bookmark. Python's docs site also offers downloadable formats from its download page.

Quick reference checklist#

  • dumps / loads for strings, dump / load for files.
  • indent=2 or indent=4 for readable output; separators=(',', ':') for compact.
  • sort_keys=True when you want stable, diff-friendly output.
  • default= or a JSONEncoder subclass for datetimes, Decimal and your own classes.
  • object_hook, parse_float and parse_int to rebuild custom types on load.
  • Expect dict keys to become strings and tuples to become lists.
  • Use ensure_ascii=False for readable non-ASCII text, and open files as UTF-8.
  • Catch json.JSONDecodeError and cap input size for untrusted data.
  • One dump() per file. For several records, write a list.
  • python -m json file.json to validate and format from the terminal.

For the full parameter list and the Python-to-JSON conversion tables, see the json module docs. If you need to query or reshape JSON from the shell rather than from Python, jq is the tool I'd reach for.

Quick answers

What is the difference between json.dump and json.dumps?

dumps() returns the JSON as a string. dump() writes it to a file-like object that has a .write() method accepting str. The same split applies to loads() and load() when reading.

How do I pretty print JSON in Python?

Pass indent to json.dumps(), for example json.dumps(data, indent=4, sort_keys=True). From the terminal, pipe JSON into python -m json or pass it a file name.

How do I serialize a datetime to JSON in Python?

The encoder doesn't handle datetime by default. Pass a default= function that returns obj.isoformat(), or subclass json.JSONEncoder and override default().

Why does json.loads raise JSONDecodeError?

The input isn't valid JSON. Common causes are single quotes, trailing commas, Python values like True or None, and empty strings. The exception's message, lineno and colno point to where parsing failed.

Indra Wijaya

Builds and runs AI agents, bots and content pipelines, and writes down what works and what broke.

About · Email · RSS

Keep reading

New notes, no spam.

One post at a time, only when there's something worth reading. Follow in any feed reader.

Subscribe via RSS