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 json module 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.

FunctionDirectionWorks with
json.dumps(obj)Python to JSONstring
json.loads(text)JSON to Pythonstring
json.dump(obj, file)Python to JSONfile
json.load(file)JSON to Pythonfile

Step 1: Convert Between Python and JSON Strings

Save this as 01_basics.py:

  1. import json
  2.  
  3. # A Python dictionary with the most common value types
  4. student = {
  5.     "id": 101,
  6.     "name": "Maria Santos",
  7.     "course": "BSIT",
  8.     "gpa": 1.75,
  9.     "enrolled": True,
  10.     "scholarship": None,
  11.     "subjects": ["Web Development", "Database Systems"],
  12. }
  13.  
  14. # dumps(): Python object -> JSON string
  15. text = json.dumps(student)
  16. print(type(text).__name__)
  17. print(text)
  18.  
  19. # loads(): JSON string -> Python object
  20. data = json.loads(text)
  21. print(type(data).__name__, data["name"], data["subjects"][0])
  22.  
  23. # Pretty printing with indent and sorted keys
  24. print(json.dumps({"b": 2, "a": 1, "list": [1, 2]}, indent=2, sort_keys=True))
  25.  
  26. # Non-English text: ensure_ascii=False keeps characters readable
  27. city = {"city": "Parañaque"}
  28. print(json.dumps(city))
  29. print(json.dumps(city, ensure_ascii=False))
  30.  
  31. # Tuples become lists, and integer keys become strings
  32. print(json.dumps({1: (10, 20)}))
  33. 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:

  • True became true and None became null. JSON has its own spelling for these values, and loads() converts them back.
  • indent=2 adds line breaks and spaces so humans can read the result. sort_keys=True orders 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. Use ensure_ascii=False when 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 1 came 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:

  1. import json
  2. from pathlib import Path
  3.  
  4. path = Path("students.json")
  5.  
  6. students = [
  7.     {"id": 1, "name": "Ana Cruz", "grade": 91},
  8.     {"id": 2, "name": "Ben Reyes", "grade": 84},
  9. ]
  10.  
  11. # dump(): write Python data to a JSON file
  12. with path.open("w", encoding="utf-8") as f:
  13.     json.dump(students, f, indent=2)
  14. print("Saved", path, path.stat().st_size, "bytes")
  15.  
  16. # load(): read the JSON file back into Python
  17. with path.open("r", encoding="utf-8") as f:
  18.     loaded = json.load(f)
  19. print("Loaded", len(loaded), "students")
  20.  
  21. # Update the data and save it again
  22. loaded.append({"id": 3, "name": "Carla Lim", "grade": 88})
  23. for s in loaded:
  24.     if s["id"] == 2:
  25.         s["grade"] = 86
  26.  
  27. with path.open("w", encoding="utf-8") as f:
  28.     json.dump(loaded, f, indent=2)
  29.  
  30. print(path.read_text(encoding="utf-8"))
  31. average = sum(s["grade"] for s in loaded) / len(loaded)
  32. 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:

  1. import json
  2. from dataclasses import dataclass, asdict
  3. from datetime import date, datetime
  4. from decimal import Decimal
  5.  
  6. order = {"id": 5001, "date": date(2026, 11, 8), "total": Decimal("1499.50")}
  7.  
  8. # This fails because json does not know how to convert date or Decimal
  9. try:
  10.     json.dumps(order)
  11. except TypeError as e:
  12.     print("TypeError:", e)
  13.  
  14. # Fix 1: a default= function that converts unknown types
  15. def to_json(value):
  16.     if isinstance(value, (date, datetime)):
  17.         return value.isoformat()
  18.     if isinstance(value, Decimal):
  19.         return str(value)  # keep exact money value as a string
  20.     raise TypeError(f"Cannot serialize {type(value).__name__}")
  21.  
  22. print(json.dumps(order, default=to_json))
  23.  
  24. # Fix 2: dataclasses -> dict with asdict()
  25. @dataclass
  26. class Product:
  27.     sku: str
  28.     name: str
  29.     price: float
  30.     tags: list
  31.  
  32. p = Product("P-100", "USB Keyboard", 450.0, ["computer", "accessories"])
  33. print(json.dumps(asdict(p)))
  34.  
  35. # Going back: object_hook turns dicts into objects while loading
  36. def as_product(d):
  37.     if {"sku", "name", "price", "tags"} <= d.keys():
  38.         return Product(**d)
  39.     return d
  40.  
  41. back = json.loads(json.dumps(asdict(p)), object_hook=as_product)
  42. print(back)
  43. print(back.name, back.price)
  44.  
  45. # parse_float=Decimal keeps money values exact when loading
  46. money = json.loads('{"price": 0.1, "qty": 3}', parse_float=Decimal)
  47. 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:

  1. import json
  2.  
  3. bad_inputs = [
  4.     '{"name": "Ana", "age": 20,}',      # trailing comma
  5.     "{'name': 'Ana'}",                  # single quotes
  6.     '{"name": "Ana" "age": 20}',        # missing comma
  7.     '',                                 # empty string
  8. ]
  9.  
  10. for text in bad_inputs:
  11.     try:
  12.         json.loads(text)
  13.     except json.JSONDecodeError as e:
  14.         print(f"{text!r:32} -> {e.msg} (line {e.lineno}, column {e.colno})")
  15.  
  16. # A safe loader for config files
  17. def load_config(text, defaults):
  18.     try:
  19.         data = json.loads(text)
  20.     except json.JSONDecodeError as e:
  21.         print("Config error:", e)
  22.         return dict(defaults)
  23.     if not isinstance(data, dict):
  24.         print("Config error: top level must be an object")
  25.         return dict(defaults)
  26.     return {**defaults, **data}
  27.  
  28. defaults = {"debug": False, "port": 8000}
  29. print(load_config('{"port": 9000}', defaults))
  30. print(load_config('{"port": 9000,,}', defaults))
  31. 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:

  1. import json
  2.  
  3. # A response like the ones you get from a REST API
  4. response_text = """
  5. {
  6.  "status": "ok",
  7.  "data": {
  8.    "orders": [
  9.      {"id": 1, "customer": {"name": "Ana"}, "items": [
  10.        {"product": "Mouse", "qty": 2, "price": 250},
  11.        {"product": "Keyboard", "qty": 1, "price": 450}
  12.      ]},
  13.      {"id": 2, "customer": {"name": "Ben"}, "items": [
  14.        {"product": "Monitor", "qty": 1, "price": 6500}
  15.      ]},
  16.      {"id": 3, "customer": null, "items": []}
  17.    ]
  18.  }
  19. }
  20. """
  21.  
  22. payload = json.loads(response_text)
  23. orders = payload["data"]["orders"]
  24.  
  25. for order in orders:
  26.     # .get() and "or {}" protect us from missing or null values
  27.     name = (order.get("customer") or {}).get("name", "(guest)")
  28.     total = sum(i["qty"] * i["price"] for i in order.get("items", []))
  29.     print(f"Order #{order['id']:<3} {name:<8} items={len(order['items'])} total={total:>6}")
  30.  
  31. summary = {
  32.     "order_count": len(orders),
  33.     "grand_total": sum(i["qty"] * i["price"] for o in orders for i in o["items"]),
  34.     "products": sorted({i["product"] for o in orders for i in o["items"]}),
  35. }
  36. 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 load and loads: pass a file object to load() and a string to loads().
  • "Object of type X is not JSON serializable": add a default= function, or convert the value first (for example asdict() for dataclasses or .isoformat() for dates).
  • Garbled characters in the file: open files with encoding="utf-8" and use ensure_ascii=False if 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!