Python

Read and Write JSON Files in Python: json.load, json.dump and Common Errors

Python · · Published 2026-11-02

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

FunctionInputOutput
json.load(f)open filePython object
json.loads(s)string (or bytes)Python object
json.dump(obj, f)Python object + open filewrites to the file
json.dumps(obj)Python objectstring

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)

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

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