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:
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().
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:
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.
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
indentmeans that many spaces per level. A string such as"\t"is used for each level instead. - If
indentis0, negative, or"", you only get newlines. - The default separators are
(', ', ': ')withoutindent, 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:
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.
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=:
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:
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.
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:
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.
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/loadsfor strings,dump/loadfor files.indent=2orindent=4for readable output;separators=(',', ':')for compact.sort_keys=Truewhen you want stable, diff-friendly output.default=or aJSONEncodersubclass for datetimes,Decimaland your own classes.object_hook,parse_floatandparse_intto rebuild custom types on load.- Expect dict keys to become strings and tuples to become lists.
- Use
ensure_ascii=Falsefor readable non-ASCII text, and open files as UTF-8. - Catch
json.JSONDecodeErrorand cap input size for untrusted data. - One
dump()per file. For several records, write a list. python -m json file.jsonto 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.
Next note