Python's standard library includes a json module, so working with JSON in Python needs no extra package. It handles both directions: JSON text into Python objects, and Python objects back into JSON text.
This guide covers the four core functions, how Python types map to JSON, pretty-printing options, and the parts that cause trouble 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, Working with JSON in JavaScript covers the same ideas with parse and stringify.
The four core functions: load, loads, dump, dumps
The json module has two pairs of functions. The s suffix means "string". loads and dumps work with data in memory. load and dump work with file-like objects. Once this is clear, the rest of the module follows.
| Function | Direction | Works with |
|---|---|---|
| json.loads(s) | JSON to Python | a str, bytes, or bytearray |
| json.dumps(obj) | Python to JSON | returns a str |
| json.load(fp) | JSON to Python | a file object |
| json.dump(obj, fp) | Python to JSON | writes to a file object |
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 versions when you read or write a file directly. You skip building the whole document as an intermediate string in your own code.
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 comes from three main places, and each one ends up at json.loads or json.load.
From an API response
If you use the requests library, the response object has a .json() method that parses the body for you. Without it, decode and parse the text yourself.
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. The exception has .msg, .lineno, .colno and .pos, so you can report exactly where the problem is.
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 translates types. 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 larger type system gets flattened on the way out.
| Python | JSON |
|---|---|
| dict | object |
| list, tuple | array |
| str | string |
| int, float | number |
| True / False | true / false |
| None | null |
Decoding runs the mapping in reverse. JSON objects become dict, arrays become list, true/false become True/False, and null becomes None. The one to remember is the tuple. It serializes to a JSON array, and no JSON type decodes to a tuple, so it comes back as a list. For more on JSON's value types, see JSON Data Types Explained.
Pretty-printing with indent and sort_keys
By default json.dumps writes everything on one line, with , and : as separators. For readable output, pass indent. For stable output that diffs cleanly, add sort_keys=True.
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"
# ]
# }Use sort_keys=True whenever JSON output is committed to version control or compared across runs. With sorted keys, the same data always produces identical bytes, so diffs show real changes instead of reordering noise.
To shrink JSON for transport, pass a tight separators tuple. It removes the spaces dumps adds by default:
import json
json.dumps(data, separators=(",", ":"))
# {"name":"Ada","roles":["admin","dev"],"active":true}For when compact or indented output matters, see How to Format and Beautify JSON and How to Minify JSON.
ensure_ascii and Unicode output
This one surprises most people. ensure_ascii defaults to True, so every non-ASCII character is escaped as a \uXXXX sequence. The result is valid JSON and safe for any transport, but non-Latin text becomes unreadable.
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 for readable output, but then you must control the encoding yourself. When writing to a file, open it with encoding="utf-8". Otherwise Python may use a platform encoding that cannot represent the characters and raise a UnicodeEncodeError.
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 can serialize the built-in JSON types and nothing else. Give 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 that json calls for any object it cannot handle. Return something serializable, or raise TypeError to reject it.
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 from .isoformat() is the standard choice. It sorts correctly, is unambiguous, and every language can parse it. The conversion only goes one way. When you read the JSON back, "2026-07-12T09:30:00" comes back as a plain str, and you convert it with datetime.fromisoformat() yourself.
Decimal loses precision when cast to float
Converting Decimal to float brings back the binary floating-point rounding that Decimal was meant to avoid. For money and other exact values, serialize with str(obj) instead. Read it back with json.loads(text, parse_float=Decimal) so numbers become Decimal rather than float.
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. Logs, data exports and streaming APIs use it because you can process one record at a time without loading a multi-gigabyte file into memory. The file as a whole is not one JSON document. Each line is. So you call json.loads on each line, not json.load on the file.
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"])read_ndjson is a generator, so memory use stays flat however large the file is. Only one parsed record exists at a time. Writing NDJSON works the same way: dump each object with dumps and add a newline. Keep ensure_ascii and separators the same for every line.
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. For how it compares to JSON5 and JSONC, see JSON5, JSONC, and NDJSON Explained.