Search
Python JSON Tutorial: dumps, loads, dump, load, Pretty Print and Error Handling
JSON is everywhere in modern programming. REST APIs send it, JavaScript front ends expect it, configuration files are written in it, and many capstone projects store small amounts of data in a .json file before they move to a real database. If you write Python, sooner or later you will need to turn a dictionary into JSON or read a JSON response back into Python.
The good news is that Python has a built-in json module, so there is nothing to install. In this Python JSON tutorial you will learn the four main functions (dumps, loads, dump and load), how to pretty print, how to read and update a JSON file, how to handle dates and Decimal values, how to catch broken JSON safely, and how to pull data out of a nested API response. Every script below was run on Python 3.13, and the output under each one is copied straight from the terminal.
Requirements
- Python 3.8 or newer (the
jsonmodule is part of the standard library). - Any code editor: VS Code, PyCharm, Thonny or IDLE.
- Basic Python: dictionaries, lists, loops and functions.
Download the example zip at the bottom of this page, extract it, open a terminal in the folder and run each script with python 01_basics.py (use python3 on macOS and Linux).
The Four Functions You Need to Know
The names look alike, so here is the easy way to remember them: the functions that end with s work with strings, and the ones without s work with files.
| Function | Direction | Works with |
|---|---|---|
json.dumps(obj) | Python to JSON | string |
json.loads(text) | JSON to Python | string |
json.dump(obj, file) | Python to JSON | file |
json.load(file) | JSON to Python | file |
Step 1: Convert Between Python and JSON Strings
Save this as 01_basics.py:
- import json
- # A Python dictionary with the most common value types
- student = {
- "id": 101,
- "name": "Maria Santos",
- "course": "BSIT",
- "gpa": 1.75,
- "enrolled": True,
- "scholarship": None,
- "subjects": ["Web Development", "Database Systems"],
- }
- # dumps(): Python object -> JSON string
- text = json.dumps(student)
- print(type(text).__name__)
- print(text)
- # loads(): JSON string -> Python object
- data = json.loads(text)
- print(type(data).__name__, data["name"], data["subjects"][0])
- # Pretty printing with indent and sorted keys
- print(json.dumps({"b": 2, "a": 1, "list": [1, 2]}, indent=2, sort_keys=True))
- # Non-English text: ensure_ascii=False keeps characters readable
- city = {"city": "Parañaque"}
- print(json.dumps(city))
- print(json.dumps(city, ensure_ascii=False))
- # Tuples become lists, and integer keys become strings
- print(json.dumps({1: (10, 20)}))
- print(json.loads(json.dumps({1: (10, 20)})))
Output:
str
{"id": 101, "name": "Maria Santos", "course": "BSIT", "gpa": 1.75, "enrolled": true, "scholarship": null, "subjects": ["Web Development", "Database Systems"]}
dict Maria Santos Web Development
{
"a": 1,
"b": 2,
"list": [
1,
2
]
}
{"city": "Para\u00f1aque"}
{"city": "Parañaque"}
{"1": [10, 20]}
{'1': [10, 20]}
A few things to notice in the output:
TruebecametrueandNonebecamenull. JSON has its own spelling for these values, andloads()converts them back.indent=2adds line breaks and spaces so humans can read the result.sort_keys=Trueorders the keys, which is handy when you compare two JSON files.- By default, non-ASCII characters such as the ñ in Parañaque are escaped as
\u00f1. Useensure_ascii=Falsewhen you want readable text, and save the file as UTF-8. - JSON has no tuples and only string keys. The tuple came back as a list and the key
1came back as the string'1'. Keep this in mind when you round-trip data.
Step 2: Write and Read a JSON File
For files, use dump() and load() with an open file object. Always pass encoding="utf-8" so the file behaves the same on Windows, macOS and Linux. Save this as 02_files.py:
- import json
- from pathlib import Path
- path = Path("students.json")
- students = [
- {"id": 1, "name": "Ana Cruz", "grade": 91},
- {"id": 2, "name": "Ben Reyes", "grade": 84},
- ]
- # dump(): write Python data to a JSON file
- with path.open("w", encoding="utf-8") as f:
- json.dump(students, f, indent=2)
- print("Saved", path, path.stat().st_size, "bytes")
- # load(): read the JSON file back into Python
- with path.open("r", encoding="utf-8") as f:
- loaded = json.load(f)
- print("Loaded", len(loaded), "students")
- # Update the data and save it again
- loaded.append({"id": 3, "name": "Carla Lim", "grade": 88})
- for s in loaded:
- if s["id"] == 2:
- s["grade"] = 86
- with path.open("w", encoding="utf-8") as f:
- json.dump(loaded, f, indent=2)
- print(path.read_text(encoding="utf-8"))
- average = sum(s["grade"] for s in loaded) / len(loaded)
- print(f"Average grade: {average:.2f}")
Output:
Saved students.json 127 bytes
Loaded 2 students
[
{
"id": 1,
"name": "Ana Cruz",
"grade": 91
},
{
"id": 2,
"name": "Ben Reyes",
"grade": 86
},
{
"id": 3,
"name": "Carla Lim",
"grade": 88
}
]
Average grade: 88.33
This load, change, save pattern is all you need for small things like a settings file, a list of high scores or a simple to-do list. When the data grows, or when several users write to it at the same time, move it to SQLite or MySQL instead.
Step 3: Handle Dates, Decimal and Your Own Classes
The json module only understands dictionaries, lists, strings, numbers, booleans and None. Anything else, such as a date or a Decimal, raises a TypeError. You fix that with the default= argument when saving, and with object_hook= or parse_float= when loading. Save this as 03_custom.py:
- import json
- from dataclasses import dataclass, asdict
- from datetime import date, datetime
- from decimal import Decimal
- order = {"id": 5001, "date": date(2026, 11, 8), "total": Decimal("1499.50")}
- # This fails because json does not know how to convert date or Decimal
- try:
- json.dumps(order)
- except TypeError as e:
- print("TypeError:", e)
- # Fix 1: a default= function that converts unknown types
- def to_json(value):
- if isinstance(value, (date, datetime)):
- return value.isoformat()
- if isinstance(value, Decimal):
- return str(value) # keep exact money value as a string
- raise TypeError(f"Cannot serialize {type(value).__name__}")
- print(json.dumps(order, default=to_json))
- # Fix 2: dataclasses -> dict with asdict()
- @dataclass
- class Product:
- sku: str
- name: str
- price: float
- tags: list
- p = Product("P-100", "USB Keyboard", 450.0, ["computer", "accessories"])
- print(json.dumps(asdict(p)))
- # Going back: object_hook turns dicts into objects while loading
- def as_product(d):
- if {"sku", "name", "price", "tags"} <= d.keys():
- return Product(**d)
- return d
- back = json.loads(json.dumps(asdict(p)), object_hook=as_product)
- print(back)
- print(back.name, back.price)
- # parse_float=Decimal keeps money values exact when loading
- money = json.loads('{"price": 0.1, "qty": 3}', parse_float=Decimal)
- print(money["price"] * money["qty"], 0.1 * 3)
Output:
TypeError: Object of type date is not JSON serializable
{"id": 5001, "date": "2026-11-08", "total": "1499.50"}
{"sku": "P-100", "name": "USB Keyboard", "price": 450.0, "tags": ["computer", "accessories"]}
Product(sku='P-100', name='USB Keyboard', price=450.0, tags=['computer', 'accessories'])
USB Keyboard 450.0
0.3 0.30000000000000004
The last line shows why money values deserve special care. With normal floats, 0.1 * 3 gives 0.30000000000000004. Loading with parse_float=Decimal keeps the exact value 0.3. For prices, the same idea applies when saving: we store Decimal("1499.50") as the string "1499.50" so no precision is lost.
Step 4: Catch Invalid JSON with JSONDecodeError
Real data is not always clean. A hand-edited config file might have a trailing comma, or an API might return an empty body. json.loads() raises json.JSONDecodeError, which tells you the message, line and column. Save this as 04_errors.py:
- import json
- bad_inputs = [
- '{"name": "Ana", "age": 20,}', # trailing comma
- "{'name': 'Ana'}", # single quotes
- '{"name": "Ana" "age": 20}', # missing comma
- '', # empty string
- ]
- for text in bad_inputs:
- try:
- json.loads(text)
- except json.JSONDecodeError as e:
- print(f"{text!r:32} -> {e.msg} (line {e.lineno}, column {e.colno})")
- # A safe loader for config files
- def load_config(text, defaults):
- try:
- data = json.loads(text)
- except json.JSONDecodeError as e:
- print("Config error:", e)
- return dict(defaults)
- if not isinstance(data, dict):
- print("Config error: top level must be an object")
- return dict(defaults)
- return {**defaults, **data}
- defaults = {"debug": False, "port": 8000}
- print(load_config('{"port": 9000}', defaults))
- print(load_config('{"port": 9000,,}', defaults))
- print(load_config('[1, 2, 3]', defaults))
Output:
'{"name": "Ana", "age": 20,}' -> Illegal trailing comma before end of object (line 1, column 26)
"{'name': 'Ana'}" -> Expecting property name enclosed in double quotes (line 1, column 2)
'{"name": "Ana" "age": 20}' -> Expecting ',' delimiter (line 1, column 16)
'' -> Expecting value (line 1, column 1)
{'debug': False, 'port': 9000}
Config error: Expecting property name enclosed in double quotes: line 1 column 15 (char 14)
{'debug': False, 'port': 8000}
Config error: top level must be an object
{'debug': False, 'port': 8000}
Two classic beginner mistakes appear here: trailing commas and single quotes. Both are fine in Python code but not allowed in JSON. The load_config() function shows a safe pattern: try to parse, fall back to defaults on error, and check that the top level is really an object before merging it.
Step 5: Read a Nested API Response
Most of the JSON you will meet comes from APIs, and it is usually nested: objects inside lists inside objects. Here is a realistic example you can run offline. Save it as 05_api_response.py:
- import json
- # A response like the ones you get from a REST API
- response_text = """
- {
- "status": "ok",
- "data": {
- "orders": [
- {"id": 1, "customer": {"name": "Ana"}, "items": [
- {"product": "Mouse", "qty": 2, "price": 250},
- {"product": "Keyboard", "qty": 1, "price": 450}
- ]},
- {"id": 2, "customer": {"name": "Ben"}, "items": [
- {"product": "Monitor", "qty": 1, "price": 6500}
- ]},
- {"id": 3, "customer": null, "items": []}
- ]
- }
- }
- """
- payload = json.loads(response_text)
- orders = payload["data"]["orders"]
- for order in orders:
- # .get() and "or {}" protect us from missing or null values
- name = (order.get("customer") or {}).get("name", "(guest)")
- total = sum(i["qty"] * i["price"] for i in order.get("items", []))
- print(f"Order #{order['id']:<3} {name:<8} items={len(order['items'])} total={total:>6}")
- summary = {
- "order_count": len(orders),
- "grand_total": sum(i["qty"] * i["price"] for o in orders for i in o["items"]),
- "products": sorted({i["product"] for o in orders for i in o["items"]}),
- }
- print(json.dumps(summary, indent=2))
Output:
Order #1 Ana items=2 total= 950
Order #2 Ben items=1 total= 6500
Order #3 (guest) items=0 total= 0
{
"order_count": 3,
"grand_total": 7450,
"products": [
"Keyboard",
"Monitor",
"Mouse"
]
}
The important line is (order.get("customer") or {}).get("name", "(guest)"). Order #3 has "customer": null, so a direct order["customer"]["name"] would crash with a TypeError. Using .get() plus a fallback keeps your script running when a field is missing or empty. If you fetch the data with the requests library, response.json() gives you the same Python dictionary that json.loads() returns here.
Bonus: Pretty Print a JSON File from the Terminal
Python ships a small command-line tool for formatting and validating JSON. Given a one-line sample.json:
{"name":"Ana","skills":["php","sql"]}
run python -m json.tool sample.json:
{
"name": "Ana",
"skills": [
"php",
"sql"
]
}
If the file is invalid, the tool prints the error with the line and column instead, which makes it a quick way to check a config file before your program reads it.
Common Mistakes and Quick Fixes
- Mixing up
loadandloads: pass a file object toload()and a string toloads(). - "Object of type X is not JSON serializable": add a
default=function, or convert the value first (for exampleasdict()for dataclasses or.isoformat()for dates). - Garbled characters in the file: open files with
encoding="utf-8"and useensure_ascii=Falseif you want readable text. - KeyError on API data: use
.get()with a default value for fields that may be missing. - Using JSON as a database: fine for small data, but switch to SQLite once you need searching, many records or several users.
Conclusion
You now know how to work with JSON in Python using only the standard library: dumps and loads for strings, dump and load for files, indent and sort_keys for readable output, default= and object_hook= for custom types, and JSONDecodeError for broken input. These skills carry straight over to calling REST APIs, saving app settings and passing data between a Python back end and a JavaScript front end.
Download the example scripts below, run them, then try changing the data to match your own project. Happy coding!