Skip to content
Python

Python json Module: load, loads, dump, and dumps Explained

·10 min read
Python json Module: load, loads, dump, and dumps Explained - Python guide from JSON Prettifier

Quick answer: Python's built-in json module has four functions. json.loads() parses a string and json.load() parses an open file; json.dumps() returns a JSON string and json.dump() writes to an open file. The trailing s means string. Add indent=2 for readable output and a default= function to serialize types JSON does not know, such as datetime.

You call json.load("settings.json"), Python answers with AttributeError: 'str' object has no attribute 'read', and you spend ten minutes convinced the file is corrupt. It is not. The function wanted a file object, not a path, and the sibling with the s wanted a string. That one letter is the source of most first-week confusion with the module, so it is where this guide starts.

Explore the resulting JSON in the JSON Viewer, or select nested fields with the JSONPath Query Tester.

load vs loads, dump vs dumps: The One-Letter Rule

The s suffix stands for string. Functions with it work on strings in memory; functions without it work on file objects. Everything else about the pair is identical, including every keyword argument.

FunctionInputReturnsTypical use
json.load(fp)File object (opened for reading)Python objectRead a .json file
json.loads(s)str, bytes, or bytearrayPython objectParse an API response body
json.dump(obj, fp)Python object + file objectNothing (writes to file)Save config to disk
json.dumps(obj)Python objectstrBuild a request body or log line

A memory trick that sticks: load and dump talk to a file, loads and dumps talk to a string. If you are holding a path, you need open() first.

Reading JSON From a File With json.load()

import json

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

print(settings["theme"])        # "dark"
print(type(settings))           # <class 'dict'>

Two details matter here. The with block closes the file even if parsing fails. And encoding="utf-8" is not optional on Windows, where the default encoding is often a legacy code page; without it, any non-ASCII character in the file can raise UnicodeDecodeError before the JSON parser even starts.

If the file might have a byte order mark (common when it was saved from Notepad or exported by PowerShell), use encoding="utf-8-sig" instead. Plain utf-8 leaves the BOM in place and the parser fails on the invisible first character.

Parsing a JSON String With json.loads()

This is the function for API bodies, message queue payloads, and anything already in a variable. json.loads() accepts str, bytes, and bytearray, so you can pass a raw HTTP body without decoding it yourself:

import json

raw = '{"id": 42, "tags": ["new", "sale"], "price": 19.99, "stock": null}'
item = json.loads(raw)

item["id"]      # 42        (int)
item["tags"]    # ['new', 'sale']  (list)
item["price"]   # 19.99     (float)
item["stock"]   # None

One thing people expect but do not get: json.loads() parses exactly one JSON value. If the string contains two objects back to back, or a trailing comment, you get JSONDecodeError: Extra data. Formats like JSON Lines need a loop that calls loads() once per line.

Writing JSON With json.dump() and json.dumps()

By default the output is compact and on one line, which is right for network payloads and wrong for anything a person reads. The three keyword arguments you will reach for most:

import json

report = {"name": "Zoë", "total": 3, "ok": True, "items": [1, 2, 3]}

json.dumps(report)
# {"name": "Zo\u00eb", "total": 3, "ok": true, "items": [1, 2, 3]}

json.dumps(report, indent=2, sort_keys=True, ensure_ascii=False)
# {
#   "items": [
#     1,
#     2,
#     3
#   ],
#   "name": "Zoë",
#   "ok": true,
#   "total": 3
# }
  • indent=2 (or 4) pretty-prints with nested indentation. Note that lists are expanded one element per line; there is no built-in option to keep short arrays compact.
  • sort_keys=True orders keys alphabetically at every level. Use it for files under version control so diffs only show real changes. The Sort Keys option in JSON Prettifier does the same thing when you are comparing output by eye.
  • ensure_ascii=False keeps non-ASCII characters literal instead of escaping them as \uXXXX. Pair it with encoding="utf-8" on the file.

Writing to disk is the same call with a file object:

with open("report.json", "w", encoding="utf-8") as f:
    json.dump(report, f, indent=2, ensure_ascii=False)
    f.write("\n")   # trailing newline keeps git and linters happy

For the smallest possible output, pass separators=(",", ":") to drop the space after each comma and colon. That is all minifying JSON amounts to.

Type Mapping: What Python Becomes and What Comes Back

Encoding (Python to JSON):

PythonJSON
dictobject
list, tuplearray
strstring
int, floatnumber
True / Falsetrue / false
Nonenull

Decoding (JSON to Python):

JSONPython
objectdict
arraylist
stringstr
number (no fraction or exponent)int
number (with fraction or exponent)float
true / falseTrue / False
nullNone

The round trip is lossy in a few places. A tuple comes back as a list. Dict keys that were integers come back as strings, because JSON object keys are always strings: json.loads(json.dumps({1: "a"})) returns {'1': 'a'}. And float("nan") is written as the bare word NaN, which is valid for Python but not for the JSON spec, so other parsers reject it. Pass allow_nan=False to make Python raise instead of producing invalid output.

Serializing datetime, Decimal, and Custom Classes With default=

The error everyone hits eventually:

>>> import json
>>> from datetime import datetime
>>> json.dumps({"when": datetime.now()})
TypeError: Object of type datetime is not JSON serializable

JSON has no date type, so the module will not guess. The default argument is a function the encoder calls for any object it cannot handle. Return something serializable or raise TypeError:

import json
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID

def to_json(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()          # "2026-02-02T09:30:00"
    if isinstance(obj, Decimal):
        return str(obj)                 # keeps exact digits
    if isinstance(obj, UUID):
        return str(obj)
    if isinstance(obj, set):
        return sorted(obj)
    raise TypeError(f"Cannot serialize {type(obj).__name__}")

json.dumps({"when": datetime(2026, 2, 2, 9, 30), "amount": Decimal("19.99")}, default=to_json)
# {"when": "2026-02-02T09:30:00", "amount": "19.99"}

ISO 8601 strings are the convention for dates in JSON; the JSON date format guide explains why and how to include a timezone. For Decimal, a string is the safe choice because converting to float silently loses precision on money values.

Dataclasses are a two-liner: default=lambda o: dataclasses.asdict(o). For larger projects, aJSONEncoder subclass with an overridden default() method, passed as cls=MyEncoder, does the same job in a reusable form.

Custom Decoding With object_hook and parse_float

Decoding has the mirror image. object_hook is called with every decoded dict, innermost first, and whatever it returns replaces that dict:

import json
from datetime import datetime

def revive(d):
    if "when" in d:
        d["when"] = datetime.fromisoformat(d["when"])
    return d

event = json.loads('{"name": "deploy", "when": "2026-02-02T09:30:00"}', object_hook=revive)
event["when"].year    # 2026

Two related hooks are worth knowing. parse_float=Decimal makes every JSON number with a fraction arrive as a Decimal, which is the correct way to read prices. And object_pairs_hook receives a list of (key, value) pairs instead of a dict, so you can detect duplicate keys that a plain dict would silently overwrite.

Handling JSONDecodeError

Malformed input raises json.JSONDecodeError, a subclass of ValueError. The exception carries the position, which is the part worth logging:

import json

text = '{"a": 1,\n "b": [1, 2,]}'

try:
    json.loads(text)
except json.JSONDecodeError as e:
    print(e.msg)      # Expecting value
    print(e.lineno)   # 2
    print(e.colno)    # 13
    print(e.pos)      # 21   (character offset from the start)

lineno and colno are 1-based, matching what editors show. The messages are terse but consistent: Expecting value usually means a trailing comma or empty input, Expecting property name enclosed in double quotes means single quotes or a trailing comma before }, and Extra data means something follows the first complete value. If the text is more than a few lines, pasting it into JSON Prettifier gives you the same line and column with the offending character highlighted, which is faster than counting.

Common Mistakes

1. Passing a path to json.load()

json.load("data.json")
# AttributeError: 'str' object has no attribute 'read'

load() wants a file object. Either open the file first or, for a one-liner, use json.loads(Path("data.json").read_text(encoding="utf-8")).

2. Feeding Python syntax to json.loads()

json.loads("{'name': 'Ada', 'active': True, 'manager': None}")
# JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

That string is a Python dict literal, not JSON: single quotes, True, and None are all invalid. If you really have a Python literal in a string, ast.literal_eval() parses it safely. The common JSON errors guide covers the rest of this family.

3. Using json.dumps() where you meant print()

json.dumps(obj) returns a string and does nothing else. If nothing appears in your terminal, you forgot to print it. Conversely, json.dump(obj, f) returns None, so assigning its result to a variable gives you nothing useful.

4. Writing with the default encoding

open("out.json", "w") without encoding= uses the platform default. On Linux and macOS that is UTF-8; on Windows it is frequently not. Always pass encoding="utf-8" for both reading and writing.

5. Expecting dumps() to be a deep copy

json.loads(json.dumps(obj)) does produce an independent copy, but it turns tuples into lists, integer keys into strings, and drops anything your default= converted. Use copy.deepcopy() when you want a real copy.

A Pattern Worth Copying

A small pair of helpers that bakes in the right defaults for most projects:

import json
from pathlib import Path

def read_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8-sig"))

def write_json(path, data):
    text = json.dumps(data, indent=2, ensure_ascii=False, sort_keys=True, default=str)
    Path(path).write_text(text + "\n", encoding="utf-8")

default=str is a pragmatic escape hatch: anything the encoder cannot handle gets its str() form, which covers datetime, Decimal, UUID, and Path well enough for logs and config files. For API contracts, write an explicit function so a new type cannot sneak through in a surprising format. If you are coming from JavaScript, the JSON.parse() and JSON.stringify() guide shows the same ideas with the equivalent arguments.

Frequently Asked Questions

What is the difference between json.load and json.loads in Python?
json.load() reads from a file object you opened with open(), while json.loads() parses a string that is already in memory. The trailing s stands for string. The same rule applies to json.dump() (writes to a file) and json.dumps() (returns a string).
How do I pretty-print JSON in Python?
Pass indent to json.dumps() or json.dump(), for example json.dumps(data, indent=2). Add sort_keys=True to order the keys alphabetically, which makes diffs stable. Without indent, Python writes everything on one line.
Why does Python say 'Object of type datetime is not JSON serializable'?
JSON has no date type, so the json module refuses to guess. Pass a default= function that converts the datetime to a string, usually with .isoformat(), and json.dumps() will call it for every value it cannot serialize on its own.
Does Python's json module keep dictionary key order?
Yes. Since Python 3.7 dictionaries preserve insertion order, and json.loads() builds its dicts in the order keys appear in the text. json.dumps() writes keys in dict order unless you pass sort_keys=True.
How do I catch a JSON parsing error in Python?
Wrap the call in try/except json.JSONDecodeError. The exception object has .msg, .lineno, .colno, and .pos attributes, so you can report exactly where the text went wrong. JSONDecodeError is a subclass of ValueError, so older code catching ValueError still works.
Why does json.dumps() turn my accented characters into \u escapes?
ensure_ascii defaults to True, which escapes every non-ASCII character as \uXXXX. Pass ensure_ascii=False to write the characters literally, and open the output file with encoding='utf-8' so they are stored correctly.

Check the Output Before You Ship It

Paste what json.dumps() produced, validate it, and sort the keys for review. Everything runs in your browser.

Format and Validate JSON