Read and Write JSON Files in Python: json.load, json.dump and Common Errors
Python's built-in json module covers almost everything you need: json.load() reads a file into dictionaries and lists, json.dump() writes them back, and the versions ending in s (loads, dumps) do the same with strings. The traps are in the details: encodings, keys that come back as strings, types JSON does not know, and error messages that point at a single character.
All examples were run with Python 3.13. Nothing needs installing; json is part of the standard library.
The four functions
| Function | Input | Output |
|---|---|---|
json.load(f) | open file | Python object |
json.loads(s) | string (or bytes) | Python object |
json.dump(obj, f) | Python object + open file | writes to the file |
json.dumps(obj) | Python object | string |
A simple way to remember it: the s stands for string.
Write a JSON file
import json
user = {
"name": "Ada",
"languages": ["Python", "C"],
"active": True,
"manager": None,
"score": 9.5,
}
with open("user.json", "w", encoding="utf-8") as f:
json.dump(user, f, indent=2)
The file now contains:
{
"name": "Ada",
"languages": [
"Python",
"C"
],
"active": true,
"manager": null,
"score": 9.5
}
Notice the translations: True became true, None became null, and tuples, if you had any, would become arrays. Without indent, everything is written on one line, which is smaller but hard to read. Add sort_keys=True if you want keys in alphabetical order, for example to get stable diffs in version control.
Read a JSON file
with open("user.json", encoding="utf-8") as f:
user = json.load(f)
print(user["languages"][0]) # Python
print(type(user)) # <class 'dict'>
A JSON object becomes a dict, an array becomes a list. The top level does not have to be an object: json.loads('[1, 2]') returns a list. Real-world files are often deeply nested; our guide to getting values from a nested dictionary shows safe ways to dig into the result without a chain of KeyErrors.
Always pass an encoding
The JSON standard, RFC 8259, requires UTF-8 for JSON exchanged between systems. Python's open(), however, uses the platform's default encoding when you do not specify one, and on Windows that has historically not been UTF-8. Writing encoding="utf-8" on every open() call removes a whole class of bugs, the same class that produces the errors described in our article on UnicodeDecodeError when reading CSV files.
A related surprise is that dumps escapes non-ASCII characters by default:
json.dumps({"city": "Zürich"})
# '{"city": "Z\\u00fcrich"}'
json.dumps({"city": "Zürich"}, ensure_ascii=False)
# '{"city": "Zürich"}'
Both are valid JSON and decode to the same string. Use ensure_ascii=False when people will read the file, and keep the file opened as UTF-8.
Keys always come back as strings
JSON object keys must be strings, so Python converts other key types on the way out and does not convert them back:
json.loads(json.dumps({1: "a"}))
# {'1': 'a'}
If you store a dictionary keyed by integers (user IDs, years), convert the keys back after loading, for example {int(k): v for k, v in data.items()}.
"Object of type X is not JSON serializable"
The module only knows dictionaries, lists, tuples, strings, numbers, booleans and None. Dates, sets, Decimal, custom classes and NumPy numbers raise a TypeError:
import datetime
json.dumps({"when": datetime.date(2026, 11, 2)})
# TypeError: Object of type date is not JSON serializable
Pass a default function. It is called for every object the encoder cannot handle and must return something that can be encoded, or raise TypeError itself:
def to_json(obj):
if isinstance(obj, (datetime.date, datetime.datetime)):
return obj.isoformat()
if isinstance(obj, set):
return sorted(obj)
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
json.dumps({"when": datetime.date(2026, 11, 2), "tags": {"b", "a"}}, default=to_json)
# '{"when": "2026-11-02", "tags": ["a", "b"]}'
ISO 8601 strings are the most portable way to store dates. On the way back in, they stay strings until you parse them with datetime.date.fromisoformat().
For money, avoid binary floats when reading: json.loads('{"price": 19.99}', parse_float=decimal.Decimal) returns {'price': Decimal('19.99')}.
Reading errors: JSONDecodeError
When the input is not valid JSON, the module raises json.JSONDecodeError, a subclass of ValueError with lineno, colno and pos attributes that point at the problem. The three messages you will see most often:
"{'name': 'Ada'}" -> Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
'{"a": 1,}' -> Illegal trailing comma before end of object: line 1 column 8 (char 7)
'' -> Expecting value: line 1 column 1 (char 0)
- Single quotes: the text is a Python
repr, not JSON, usually because someone wrotestr(my_dict)to a file. Fix the writer to usejson.dump. Only if you cannot, and the data is trusted,ast.literal_eval()can read it. - Trailing comma: allowed in Python and JavaScript, forbidden in JSON. (Older Python versions report it as "Expecting property name enclosed in double quotes".)
- Expecting value at char 0: the input is empty or is not JSON at all. With web APIs this is typically an HTML error page or an empty body. Print the first 200 characters and the HTTP status before parsing; our guide to HTTP requests with urllib3 shows how to check both.
To catch the error and report where it happened:
try:
data = json.loads(text)
except json.JSONDecodeError as e:
print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")
raise
Several records: JSON Lines
Calling json.dump() repeatedly on the same file produces invalid JSON, because a JSON document has a single top-level value. For logs and large datasets, write one JSON value per line instead, a format known as JSON Lines (.jsonl):
events = [{"id": 1, "type": "login"}, {"id": 2, "type": "logout"}]
with open("events.jsonl", "w", encoding="utf-8") as f:
for event in events:
f.write(json.dumps(event) + "\n")
with open("events.jsonl", encoding="utf-8") as f:
events = [json.loads(line) for line in f if line.strip()]
You can append to the file safely and process it line by line without loading everything into memory. pandas reads it directly with pd.read_json("events.jsonl", lines=True).
Compact output, NaN and other options
- Smallest output:
separators=(",", ":")removes the spaces after commas and colons. - NaN and infinity: by default Python writes
NaNandInfinity, which are not valid JSON and will break strict parsers in other languages. Passallow_nan=Falseto get aValueErrorinstead, and replace those values withNonebefore writing. - Custom objects on read:
object_hookreceives every decoded dictionary and can turn it into an instance of your own class. - Untrusted input: very large or deeply nested documents can consume a lot of memory and CPU, so limit the size of what you accept from outside.
Check or pretty-print a file from the terminal
The module also works as a command-line tool, handy for validating a file or making a minified one readable:
python -m json.tool --sort-keys in.json
{
"a": [
1,
2
],
"b": 1
}
If the file is invalid, the command prints the same error message, with line and column, and exits with a non-zero status, which makes it usable in scripts and CI checks.
Summary
Use load/dump for files and loads/dumps for strings, always open files with encoding="utf-8", add indent and ensure_ascii=False for human-readable output, handle dates and sets with a default function, expect keys to come back as strings, and use JSON Lines whenever you have many records.
Sources
- Python documentation, json: JSON encoder and decoder.
- IETF, RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format (December 2017).
PythonNested dictionary values
Get values from nested dictionaries in Python: chained indexing, safe .get() chains, a deep_get helper, lists in dicts, recursion, json_normalize and glom.
WebHTTP requests with urllib3
How to send GET and POST requests with urllib3: PoolManager, query fields, JSON bodies, headers, timeouts, Retry, redirects, TLS, streaming and errors (v2).
DataUnicodeDecodeError in read_csv
Why pd.read_csv raises UnicodeDecodeError: 'utf-8' codec can't decode byte, and how to fix it with the right encoding, BOM handling and UTF-8 conversion.