JSON — JavaScript Object Notation — is the most common format for exchanging data between programs over the internet. Most web APIs speak JSON; most config files are JSON or its cousin YAML.

If you’ve worked with Python dictionaries and lists, JSON will feel completely familiar — it’s basically the same shape on disk.

The mapping between JSON and Python

JSONPython
object { }dict
array [ ]list
stringstr
numberint or float
true / falseTrue / False
nullNone

The Python json module converts between them automatically.

A sample JSON file

Save this as user.json:

{
  "name": "Manikandan",
  "age": 30,
  "skills": ["python", "go", "ml"],
  "active": true,
  "manager": null
}

Reading JSON

import json

with open("user.json", encoding="utf-8") as f:
    data: dict[str, object] = json.load(f)

print(data)
print(data["name"])
print(data["skills"])
{'name': 'Manikandan', 'age': 30, 'skills': ['python', 'go', 'ml'], 'active': True, 'manager': None}
Manikandan
['python', 'go', 'ml']

json.load(f) reads from a file. json.loads(text) (with an s) reads from a string — useful when the JSON came from an API:

text: str = '{"name": "Alice", "age": 25}'
data: dict[str, object] = json.loads(text)
print(data["name"])

Writing JSON

data: dict[str, object] = {
    "name": "Manikandan",
    "age": 30,
    "skills": ["python", "go", "ml"],
    "active": True,
    "manager": None,
}

with open("output.json", "w", encoding="utf-8") as f:
    json.dump(data, f, indent=2)
{
  "name": "Manikandan",
  "age": 30,
  "skills": [
    "python",
    "go",
    "ml"
  ],
  "active": true,
  "manager": null
}

indent=2 pretty-prints with 2-space indentation. Without it, the output is one long line — fine for machines, painful for humans.

json.dumps(data) (with an s) returns a JSON string instead of writing to a file:

text: str = json.dumps(data, indent=2)
print(text)

What you can and can’t serialise

JSON only supports the types in the table above. Things that don’t work:

  • set — not in JSON. Convert to a list first.
  • tuple — comes back as a list after a round trip.
  • datetime, custom classes — need extra handling.
  • NaN, Infinity — work in Python but aren’t strictly valid JSON.
import json
from datetime import datetime

data = {"created_at": datetime.now()}
json.dumps(data)
# TypeError: Object of type datetime is not JSON serializable

The fix is usually to convert before serialising — data["created_at"] = data["created_at"].isoformat() — or pass a custom default function. We’ll keep things simple here.

Common arguments

json.dump(data, f, indent=2)              # pretty-printed
json.dump(data, f, sort_keys=True)        # keys in alphabetical order
json.dump(data, f, ensure_ascii=False)    # keep non-ASCII characters as-is
json.dump(data, f, separators=(",", ":")) # most compact form (no spaces)

For human-readable output (configs, debug dumps), use indent=2, sort_keys=True, ensure_ascii=False. For maximum compactness (sending over the network), use separators=(",", ":").

Handling JSON errors

json.load raises json.JSONDecodeError if the file isn’t valid JSON:

try:
    with open("bad.json", encoding="utf-8") as f:
        data = json.load(f)
except json.JSONDecodeError as e:
    print(f"Bad JSON: {e}")

The error message tells you which line and column the parser tripped on — useful when debugging hand-edited config files.

A complete example — a small notes app

A program that reads notes from a JSON file, appends one, and writes it back:

import json
from pathlib import Path


def load_notes(path: Path) -> list[str]:
    if not path.exists():
        return []
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def save_notes(path: Path, notes: list[str]) -> None:
    with open(path, "w", encoding="utf-8") as f:
        json.dump(notes, f, indent=2, ensure_ascii=False)


path: Path = Path("notes.json")
notes: list[str] = load_notes(path)

new_note: str = input("New note: ")
notes.append(new_note)

save_notes(path, notes)
print(f"Total notes: {len(notes)}")

Run it a few times. Each run loads the existing list, appends one, saves it back. The notes.json file grows naturally.

JSON vs other formats

  • JSON — universal, widely supported, no comments allowed.
  • YAML — looks like JSON but with comments and cleaner syntax. Popular for configs (Docker, Kubernetes, GitHub Actions). Needs a third-party package (pyyaml).
  • TOML — the format Python itself uses for pyproject.toml. Comes built-in as tomllib (read-only) in Python 3.11+.

For exchanging data with anything outside Python: JSON. Inside a Python-only project, TOML or JSON for config.

What’s next

Last lesson of the section — pathlib, the modern way to work with file paths.

Toggle theme (T)