Working with JSON
Reviewed & published by Brayan K
JSON is the universal language of data on the internet. Every web API you'll ever call, most config files, and countless data feeds use it. Python's json module converts between JSON text and native Python objects in one line each direction.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
If you can work with dictionaries and lists, you already understand JSON — it's the same structures written as text.
What You'll Learn in This Lesson
- What JSON is and how it maps to Python types
- loads / dumps — converting strings ↔ objects
- load / dump — reading and writing JSON files
- Pretty-printing with indent and sort_keys
- Navigating nested JSON safely
- Handling non-serializable types like datetime
1 What Does JSON Look Like?
JSON is just structured text. Here's a typical API response describing a user:
{
"name": "Ada Lovelace",
"age": 36,
"is_active": true,
"roles": ["admin", "editor"],
"address": {
"city": "London",
"zip": null
}
}If that looks like a Python dictionary, that's because the structures are almost identical. The differences are small but important:
2 Strings: loads and dumps
Two functions cover converting between JSON text and Python objects:
import json
# loads: JSON STRING -> Python object ("load string")
text = '{"name": "Ada", "age": 36, "active": true}'
person = json.loads(text)
print(person) # {'name': 'Ada', 'age': 36, 'active': True}
print(person["name"]) # Ada
print(type(person)) # <class 'dict'>
# dumps: Python object -> JSON STRING ("dump string")
data = {"city": "London", "population": 8900000, "capital": True}
out = json.dumps(data)
print(out) # {"city": "London", "population": 8900000, "capital": true}
print(type(out)) # <class 'str'>Notice the round trip: true in JSON became True in Python, and back to true when dumped. The module handles all the type translation for you.
📁 Files: load and dump (no 's')
When working with files instead of strings, drop the "s". These read from / write to an open file object directly:
import json
settings = {
"theme": "dark",
"font_size": 14,
"plugins": ["spellcheck", "linter"],
}
# WRITE to a file (dump, no s)
with open("config.json", "w") as f:
json.dump(settings, f, indent=2)
# READ from a file (load, no s)
with open("config.json") as f:
loaded = json.load(f)
print(loaded["theme"]) # dark
print(loaded["plugins"]) # ['spellcheck', 'linter']🧠 The naming pattern (easy to remember):
- dumps / loads → the s is for string
- dump / load → no s → works with a file
import json
# Simulate receiving JSON text from an API
response = '''
{
"status": "ok",
"results": [
{"id": 1, "title": "Intro to Python", "published": true},
{"id": 2, "title": "Advanced JSON", "published": false}
]
}
'''
data = json.loads(response)
print("Status:", data["status"])
print("Number of results:", len(data["results"]))
# Loop through the nested list of objects
for course in data["results"]:
state = "live" if course["published"] else "draft"
print(f'- #{course["id"]} {course["title"]} ({state})')
# ✅ Expected output:
# Status: ok
# Number of results: 2
# - #1 Intro to Python (live)
# - #2 Advanced JSON (draft)3 Pretty-Printing JSON
By default dumps produces one long line — fine for machines, painful for humans. Add indent to format it nicely:
import json
data = {"name": "Ada", "skills": ["Python", "math"], "active": True}
# Compact (default)
print(json.dumps(data))
# {"name": "Ada", "skills": ["Python", "math"], "active": true}
# Pretty, sorted keys
print(json.dumps(data, indent=2, sort_keys=True))
# {
# "active": true,
# "name": "Ada",
# "skills": [
# "Python",
# "math"
# ]
# }Use indent for config files and debugging output you want to read. Skip it (compact) for network payloads where size matters.
4 The "Not JSON Serializable" Error
JSON only knows about basic types. Hand it a datetime, a set, or a custom object and you'll hit a TypeError:
import json
from datetime import datetime
record = {"event": "login", "when": datetime(2024, 1, 15, 9, 0)}
# json.dumps(record)
# TypeError: Object of type datetime is not JSON serializableimport json
from datetime import datetime
record = {"event": "login", "when": datetime(2024, 1, 15, 9, 0)}
# Fix 1: convert the value to a string yourself
record["when"] = record["when"].isoformat()
print(json.dumps(record))
# {"event": "login", "when": "2024-01-15T09:00:00"}
# Fix 2: pass a 'default' function for any unknown type
record2 = {"event": "logout", "when": datetime(2024, 1, 15, 17, 0)}
print(json.dumps(record2, default=str))
# {"event": "logout", "when": "2024-01-15 17:00:00"}🧪 Worked Example — One Full Round Trip
Here is everything from the sections above in one program: parse JSON text, navigate it safely, change it with ordinary Python, deal with a datetime, write it to a file, and read it back. This five-step shape is what almost every real JSON job looks like.
Read the comments as you go — each one says what the line produces and why it is written that way.
import json
from datetime import datetime
# ---- 1. START FROM JSON TEXT (what an API or a file hands you) -------------
raw = '''
{
"app": "notes",
"version": 3,
"features": {"dark_mode": true, "sync": false},
"tags": ["work", "personal"]
}
'''
config = json.loads(raw) # JSON text -> Python objects
print(type(config)) # a dict, nothing exotic
print(config["features"]["dark_mode"]) # JSON true arrived as Python True
# ---- 2. NAVIGATE SAFELY ---------------------------------------------------
# Square brackets raise KeyError when a key is absent. .get() returns a default
# instead — the right choice for anything optional in a payload you don't own.
print(config.get("author", "unknown")) # the key isn't there at all
print(config["features"].get("sync", False)) # the key IS there, value False
# ---- 3. CHANGE IT WITH ORDINARY PYTHON ------------------------------------
# A parsed payload is just dicts and lists. There is no special JSON API here.
config["version"] = 4
config["tags"].append("archive")
# datetime is not a JSON type, so convert it to text yourself before dumping.
# .isoformat() gives the standard "2024-06-01T12:30:00" shape.
config["updated"] = datetime(2024, 6, 1, 12, 30).isoformat()
# ---- 4. WRITE IT OUT (dump, no "s" — it takes a file object) ---------------
with open("config.json", "w") as f:
json.dump(config, f, indent=2, sort_keys=True)
# ---- 5. READ IT BACK (load, no "s") ---------------------------------------
with open("config.json") as f:
reloaded = json.load(f)
print(reloaded["version"])
print(reloaded["tags"])
print(reloaded["updated"])
print(reloaded == config) # nothing was lost on the way out and back
# ✅ Expected output:
# <class 'dict'>
# True
# unknown
# False
# 4
# ['work', 'personal', 'archive']
# 2024-06-01T12:30:00
# TrueOne thing worth noticing: reloaded == config is True only because every value survived the trip. Had you left the datetime object in place, step 4 would have raised TypeError before it ever reached the file.
🎯 Your Turn — Parse, Edit, Print
Three blanks, and all three are the names this lesson is about. Remember the rule: the s in loads and dumps stands for string.
import json
# 🎯 YOUR TURN — parse a payload, change it, print it back as JSON
# Fill in the three blanks marked ___
payload = '{"user": "ada", "score": 40, "badges": ["starter"]}'
# 👉 blank 1: the function that turns JSON TEXT into a Python dict
data = json.___(payload)
print(data["user"])
print(data["score"] + 10) # works because score parsed as a real int, not text
data["score"] = data["score"] + 10
data["badges"].append("improver")
# 👉 blank 2: the function that turns a Python object back into JSON TEXT
# 👉 blank 3: the argument that spreads it over several lines, 2 spaces deep
print(json.___(data, ___=2))
# ✅ Expected output:
# ada
# 50
# {
# "user": "ada",
# "score": 50,
# "badges": [
# "starter",
# "improver"
# ]
# }If you see TypeError: the JSON object must be str, bytes or bytearray, not dict, blanks 1 and 2 have been swapped.
Real-World Example: A Tiny To-Do App That Saves Its State
This pattern — load JSON on start, modify in memory, save JSON on exit — powers countless small tools and CLI apps:
import json
# Pretend this is what was read from tasks.json on startup
saved = '''
[
{"task": "Write lesson", "done": true},
{"task": "Review PR", "done": false},
{"task": "Reply emails", "done": false}
]
'''
tasks = json.loads(saved) # JSON text -> list of dicts
# Add a new task in memory
tasks.append({"task": "Ship release", "done": False})
# Mark the second task complete
tasks[1]["done"] = True
# Build a summary
done = sum(1 for t in tasks if t["done"])
print(f"{done}/{len(tasks)} tasks complete")
for t in tasks:
mark = "[x]" if t["done"] else "[ ]"
print(f"{mark} {t['task']}")
# On exit you'd persist it back:
output = json.dumps(tasks, indent=2)
print("\n--- saved to tasks.json ---")
print(output)The same three steps appear everywhere: json.load to read state, normal Python to change it, json.dump to save. JSON is your app's memory between runs.
import json
# Build a Python structure, then serialize it nicely
report = {
"generated": "2024-01-15",
"totals": {"users": 1240, "active": 892},
"top_pages": ["/home", "/pricing", "/docs"],
"healthy": True,
}
pretty = json.dumps(report, indent=2, sort_keys=True)
print(pretty)
# Round-trip check: dumps then loads should give back an equal object
again = json.loads(pretty)
print("\nRound-trip OK?", again == report)
print("Active users:", again["totals"]["active"])
# ✅ Expected output (sort_keys puts the keys in alphabetical order):
# {
# "generated": "2024-01-15",
# "healthy": true,
# "top_pages": [
# "/home",
# "/pricing",
# "/docs"
# ],
# "totals": {
# "active": 892,
# "users": 1240
# }
# }
#
# Round-trip OK? True
# Active users: 892🎯 Mini-Challenge: A High-Score File
Support is off now — just a brief and an outline. Build a leaderboard that loads from JSON text, gains a new entry, sorts itself, prints a ranking, then saves and reloads from a file.
# 🎯 MINI-CHALLENGE: a tiny high-score file
#
# 1. import json
# 2. Start from this text (copy it exactly):
# raw = '[{"name": "ada", "score": 40}, {"name": "grace", "score": 65}]'
# 3. Parse it into a list of dicts
# 4. Append {"name": "alan", "score": 52}
# 5. Sort by score, highest first:
# sorted(scores, key=lambda s: s["score"], reverse=True)
# 6. Print a numbered ranking, one line each, in the form: 1. grace - 65
# (enumerate(scores, start=1) gives you the numbers)
# 7. Write the sorted list to scores.json with indent=2
# 8. Read scores.json back and print how many entries returned,
# in the form: 3 entries reloaded
#
# ✅ Expected output:
# 1. grace - 65
# 2. alan - 52
# 3. ada - 40
# 3 entries reloaded
# your code hereWatch step 7: json.dump (no s) wants an open file object, not a filename. Passing the string "scores.json" gives you AttributeError: 'str' object has no attribute 'write'.
🧩 Reorder Challenge
These lines should parse JSON text and print one field, but they're scrambled. Put them in order:
A) data = json.loads(text)
B) import json
C) print(data["price"])
D) text = '{"item": "book", "price": 24.99}'Correct order: B → D → A → C
import json
text = '{"item": "book", "price": 24.99}'
data = json.loads(text)
print(data["price"]) # 24.99Import, define the JSON string, parse it into a dict with loads, then access the key. After parsing, data is a normal dict.
🧠 Quick Recall — Predict the Output
import json
print(type(json.loads('[1, 2, 3]')))<class 'list'> — a JSON array always becomes a Python list.
import json
print(json.dumps({"ok": True, "n": None})){"ok": true, "n": null} — Python's True becomes JSON true (lowercase) and None becomes null.
import json
d = json.loads('{"a": 1}')
print(d["a"] + 9)10 — the JSON number 1 is parsed as a real Python int, so arithmetic works directly. (No need to convert from a string.)
❓ Frequently Asked Questions
You speak the language of the web now!
You can parse JSON with loads/load, produce it with dumps/dump, pretty-print it, navigate nested structures, and handle tricky types like datetime. This unlocks every web API on the internet.
Practice quiz
What does JSON stand for?
- Java Standard Object Notation
- Joined Sequence Object Names
- JavaScript Object Notation
- JavaScript Ordered Nodes
Answer: JavaScript Object Notation. JSON = JavaScript Object Notation, a lightweight text format for data exchange.
A JSON object {...} becomes which Python type?
- dict
- list
- tuple
- set
Answer: dict. A JSON object maps to a Python dict; a JSON array maps to a list.
What does json.loads(text) do?
- Writes JSON to a file
- Converts a Python object into a JSON string
- Reads a JSON file
- Converts a JSON string into a Python object
Answer: Converts a JSON string into a Python object. loads ('load string') parses a JSON string into a Python object.
What does json.dumps(obj) return?
- A Python dict
- A JSON string
- None (writes to file)
- A list
Answer: A JSON string. dumps ('dump string') serializes a Python object into a JSON string.
What is the difference between json.dump and json.dumps?
- dump writes to a file; dumps returns a string
- No difference
- dumps writes to a file; dump returns a string
- dump is for reading
Answer: dump writes to a file; dumps returns a string. The 's' means string: dumps returns a string, while dump writes to a file object.
JSON true and null become which Python values?
- true and null
- 'true' and 'null'
- True and None
- 1 and 0
Answer: True and None. JSON true/false/null map to Python True/False/None.
What does json.dumps({'ok': True, 'n': None}) produce?
- {"ok": True, "n": None}
- {"ok": true, "n": null}
- {'ok': true, 'n': null}
- {"ok": 1, "n": 0}
Answer: {"ok": true, "n": null}. Python True/None become JSON true/null (lowercase) with double-quoted keys.
How do you pretty-print JSON with indentation?
- json.dumps(data, pretty=True)
- json.dumps(data, format=2)
- json.pretty(data)
- json.dumps(data, indent=2)
Answer: json.dumps(data, indent=2). Pass indent=2 (or 4) to json.dumps/dump to format with newlines and indentation.
Why does json.dumps fail on a datetime object?
- datetime is too large
- json only serializes basic types (dict, list, str, int, float, bool, None)
- datetime is private
- You must import datetime first
Answer: json only serializes basic types (dict, list, str, int, float, bool, None). json can't serialize datetime directly; convert it first (e.g. .isoformat()) or pass default=str.
Which exception does json.loads raise on invalid JSON text?
- ValueError only
- TypeError
- json.JSONDecodeError
- SyntaxError
Answer: json.JSONDecodeError. Malformed JSON raises json.JSONDecodeError, so wrap untrusted parsing in try/except.
Continue this course
- Previous: Dates & Times (datetime)
- Next: HTTP Requests & APIs — Call REST APIs and handle responses with the requests library
- Quick reference: Python cheat sheet › Standard Library
Frequently asked questions
What is JSON?
JSON (JavaScript Object Notation) is a lightweight, text-based format for storing and exchanging data. It looks like nested dictionaries and lists and is the standard format for web APIs and config files.
What is the difference between json.dumps and json.dump?
json.dumps returns a JSON string (the 's' stands for 'string'). json.dump writes JSON directly to a file object. The same naming applies to loads (from string) and load (from file).
How does Python map JSON types to its own types?
JSON objects become dicts, arrays become lists, strings stay strings, numbers become int or float, true/false become True/False, and null becomes None.
Why do I get 'Object of type ... is not JSON serializable'?
json can only serialize basic types (dict, list, str, int, float, bool, None). Objects like datetime or custom classes need to be converted first — for example turn a datetime into a string with .isoformat(), or pass a custom default function to json.dumps.
How do I make JSON output readable?
Pass indent=2 (or 4) to json.dumps/json.dump to pretty-print with indentation and newlines. Add sort_keys=True to order keys alphabetically.