{ }jsonkitOpen the tool

Working with JSON in Python: The json Module Guide

6 min read · updated 2026-07-12

Python ships with a json module in the standard library, so working with JSON needs no third-party dependency. It covers the whole round trip: turning JSON text into Python objects and turning Python objects back into JSON text.

This guide walks through the four core functions, the Python-to-JSON type mapping, pretty-printing options, and the parts that trip people up in production: non-ASCII output, serializing datetime and Decimal, and streaming large files as NDJSON. Every section has runnable code. If you also work in the browser, the companion guide Working with JSON in JavaScript covers the same ideas with parse and stringify.

The Four Core Functions: load, loads, dump, dumps

The json module exposes two pairs of functions. The s suffix means "string": loads/dumps work with data in memory, while load/dump work with file-like objects. Get this distinction right and the rest of the module follows.

FunctionDirectionWorks with
json.loads(s)JSON to Pythona str, bytes, or bytearray
json.dumps(obj)Python to JSONreturns a str
json.load(fp)JSON to Pythona file object
json.dump(obj, fp)Python to JSONwrites to a file object
Round-tripping between strings and objectspython
import json

# Parse JSON text into a Python object
data = json.loads('{"name": "Ada", "age": 36, "active": true}')
print(data["name"])   # Ada
print(type(data))     # <class 'dict'>

# Serialize a Python object back to JSON text
payload = {"name": "Ada", "roles": ["admin", "dev"], "active": True}
text = json.dumps(payload)
print(text)           # {"name": "Ada", "roles": ["admin", "dev"], "active": true}

Use the file variants when you are reading or writing a file directly. They avoid materializing the whole document as an intermediate string.

Reading and writing filespython
import json

# Read a JSON file
with open("config.json", encoding="utf-8") as f:
    config = json.load(f)

# Write a JSON file
with open("out.json", "w", encoding="utf-8") as f:
    json.dump(config, f)

Parsing JSON from a String, File, or API Response

In real code, JSON arrives from three main places, and each lands on json.loads or json.load.

From an API response

If you use the popular requests library, the response object has a .json() helper that parses the body for you. Without it, decode and parse the text yourself.

Parsing an HTTP responsepython
import json
import urllib.request

with urllib.request.urlopen("https://api.example.com/users/1") as resp:
    body = resp.read().decode("utf-8")

user = json.loads(body)
print(user["email"])

# With requests, this is simply:
# user = requests.get(url).json()

Always wrap parsing of untrusted input in try/except. Malformed JSON raises json.JSONDecodeError (a subclass of ValueError), and the exception carries .msg, .lineno, .colno, and .pos so you can report exactly where the problem is.

examplepython
import json

try:
    data = json.loads(raw_text)
except json.JSONDecodeError as e:
    print(f"Invalid JSON at line {e.lineno}, column {e.colno}: {e.msg}")

How Python Types Map to JSON

Serialization is a type translation. Knowing the mapping explains why True becomes true and why a tuple comes back as a list. JSON has only six value types, so Python's richer type system gets flattened on the way out.

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

Decoding runs the mapping in reverse: JSON objects become dict, arrays become list, true/false become True/False, and null becomes None. The asymmetry to remember is the tuple — it serializes to a JSON array, and no JSON type decodes back to a tuple, so it returns as a list. For the full breakdown of JSON's value types, see JSON Data Types Explained.

Pretty-Printing with indent and sort_keys

By default json.dumps puts everything on one line, using , and : as separators. For human-readable output, pass indent. For stable, diff-friendly output, add sort_keys=True.

Readable, deterministic outputpython
import json

data = {"name": "Ada", "roles": ["admin", "dev"], "active": True}

print(json.dumps(data, indent=2, sort_keys=True))
# {
#   "active": true,
#   "name": "Ada",
#   "roles": [
#     "admin",
#     "dev"
#   ]
# }

Adopt sort_keys=True whenever JSON output is committed to version control or compared across runs: sorted keys mean the same data always produces byte-identical text, so diffs show real changes instead of reordering noise.

Going the other direction, to shrink JSON for transport, pass a tight separators tuple to strip the whitespace dumps adds by default:

examplepython
import json

json.dumps(data, separators=(",", ":"))
# {"name":"Ada","roles":["admin","dev"],"active":true}
{ }JSON FormatterNeed to inspect or reformat JSON without running Python? Paste it into the JSON Formatter to pretty-print, collapse nodes, and validate structure in the browser.

For more on when compact versus indented output matters, see How to Format and Beautify JSON and How to Minify JSON.

ensure_ascii and Unicode Output

This one surprises almost everyone. By default ensure_ascii=True, so every non-ASCII character is escaped as a \uXXXX sequence. The result is valid JSON and safe for any transport, but it is unreadable for non-Latin text.

Keeping human-readable Unicodepython
import json

data = {"city": "Zürich", "emoji": "🚀"}

print(json.dumps(data))
# {"city": "Zürich", "emoji": "🚀"}

print(json.dumps(data, ensure_ascii=False))
# {"city": "Zürich", "emoji": "🚀"}

Set ensure_ascii=False when you want readable output — but then you must control the encoding yourself. When writing to a file, open it with encoding="utf-8"; otherwise Python may fall back to a platform encoding that cannot represent the characters and raise a UnicodeEncodeError.

examplepython
import json

with open("out.json", "w", encoding="utf-8") as f:
    json.dump(data, f, ensure_ascii=False, indent=2)

Custom Encoders: datetime, Decimal, and default=

The module knows how to serialize the built-in JSON types and nothing else. Hand it a datetime, Decimal, set, UUID, or your own class and it raises TypeError: Object of type ... is not JSON serializable. The fix is the default parameter: a function called for any object json cannot handle. Return something serializable, or raise TypeError to reject it.

A reusable default functionpython
import json
from datetime import datetime, date
from decimal import Decimal
from uuid import UUID

def json_default(obj):
    if isinstance(obj, (datetime, date)):
        return obj.isoformat()
    if isinstance(obj, Decimal):
        return float(obj)      # or str(obj) to preserve exact precision
    if isinstance(obj, UUID):
        return str(obj)
    if isinstance(obj, set):
        return sorted(obj)
    raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")

record = {
    "id": UUID("12345678-1234-5678-1234-567812345678"),
    "created": datetime(2026, 7, 12, 9, 30),
    "price": Decimal("19.99"),
    "tags": {"python", "json"},
}

print(json.dumps(record, default=json_default, indent=2))

datetime has no JSON type

JSON has no date type, so a datetime must become a string or a number. ISO 8601 via .isoformat() is the standard choice — it is sortable, unambiguous, and parseable everywhere. Remember it is one-way: when you read the JSON back, "2026-07-12T09:30:00" returns as a plain str, which you convert with datetime.fromisoformat() yourself.

Decimal loses precision when cast to float

Converting Decimal to float reintroduces the binary floating-point rounding you used Decimal to avoid. For money and other exact values, serialize with str(obj) instead, and read it back with json.loads(text, parse_float=Decimal) so numbers become Decimal rather than float.

examplepython
import json
from decimal import Decimal

text = '{"total": 19.99, "tax": 1.60}'
exact = json.loads(text, parse_float=Decimal)
print(exact["total"])          # Decimal('19.99') — exact decimal, no binary rounding
print(type(exact["total"]))    # <class 'decimal.Decimal'>

Reading and Writing NDJSON Line by Line

NDJSON (newline-delimited JSON) puts one complete JSON object on each line. It is the standard format for logs, data exports, and streaming APIs because you can process one record at a time without loading a multi-gigabyte file into memory. Crucially, the whole file is not one JSON document — each line is, so you call json.loads per line, not json.load on the file.

Streaming an NDJSON filepython
import json

def read_ndjson(path):
    with open(path, encoding="utf-8") as f:
        for line in f:
            line = line.strip()
            if not line:          # skip blank lines
                continue
            yield json.loads(line)

for event in read_ndjson("events.ndjson"):
    print(event["id"], event["type"])

Because read_ndjson is a generator, memory stays flat no matter how large the file is — only one parsed record exists at a time. Writing NDJSON mirrors this: dump each object with dumps and append a newline. Keep ensure_ascii and separators consistent so every line is uniform.

examplepython
import json

with open("events.ndjson", "w", encoding="utf-8") as f:
    for event in events:
        f.write(json.dumps(event, ensure_ascii=False))
        f.write("\n")

NDJSON is one of several JSON variants worth knowing. For how it compares to JSON5 and JSONC, see JSON5, JSONC, and NDJSON Explained.

Frequently asked questions

What is the difference between json.load and json.loads?

json.loads (with an 's' for string) parses JSON from a string or bytes already in memory. json.load reads JSON directly from a file-like object. The same pairing applies to output: dump writes to a file, while dumps returns a string.

How do I pretty-print JSON in Python?

Pass indent to json.dumps, for example json.dumps(data, indent=2). Add sort_keys=True for deterministic key ordering, and ensure_ascii=False if you want readable non-ASCII characters instead of \u escapes.

Why does json.dumps output \u escapes for accented or non-English characters?

Because ensure_ascii defaults to True, which escapes every non-ASCII character to a \uXXXX sequence. Pass ensure_ascii=False to keep the original characters, and open any output file with encoding="utf-8".

How do I serialize a datetime to JSON in Python?

JSON has no date type, so convert it to a string. Pass a default function to json.dumps that returns obj.isoformat() for datetime objects. When reading back, the value is a string you parse with datetime.fromisoformat().

How do I parse a large JSON file without loading it all into memory?

Use NDJSON (one JSON object per line) and iterate the file line by line, calling json.loads on each stripped line. A generator keeps memory flat regardless of file size. Note that a standard single-document JSON file still requires reading the whole structure at once with the built-in module.

Does Python keep dictionary order when serializing to JSON?

Yes. Since Python 3.7 dicts preserve insertion order, and json.dumps writes keys in that order. Pass sort_keys=True if you want alphabetical order instead, for stable, diff-friendly output.