Working with JSON
JSON is how programs exchange structured data. Every web API returns it, most configuration files use it, and it maps almost exactly onto the Python structures from module 4 — which is why this lesson is mostly about the places it does not.
Four functions
The whole module comes down to these:
| Function | Does |
|---|---|
json.loads(text) |
JSON string → Python |
json.dumps(obj) |
Python → JSON string |
json.load(file) |
read JSON from a file |
json.dump(obj, file) |
write JSON to a file |
The s means "string". loads and dumps work on strings in memory;
load and dump work on file objects. Mixing them up is the most common
mistake, and the error is usually AttributeError: 'str' object has no attribute 'read' — which, from module 6, you can now read as "I gave it a string where it
wanted a file".
Reading JSON
From a string:
import json
text = '{"name": "Priya", "age": 28, "skills": ["Python", "SQL"]}'
data = json.loads(text)
print(data["name"]) # Priya
print(data["skills"][0]) # Python
print(type(data)) # <class 'dict'>
It is a plain dictionary. Everything from module 4 applies — .get(), looping,
nesting.
From a file:
with open("config.json", encoding="utf-8") as file:
config = json.load(file)
Note load, not loads, because you are handing it a file.
Writing JSON
data = {"name": "Priya", "age": 28, "skills": ["Python", "SQL"]}
with open("output.json", "w", encoding="utf-8") as file:
json.dump(data, file, indent=2)
indent=2 makes it readable by humans and diffable in Git. Without it you get
one long line, which is marginally smaller and unpleasant to work with. For
files a person may open, always indent.
To a string, for printing or sending:
print(json.dumps(data, indent=2))
This is the readable-printing trick from the nesting lesson in module 4, now explained.
Two options worth knowing
json.dumps(data, indent=2, sort_keys=True)
sort_keys puts keys in alphabetical order, which makes two versions of a file
comparable. Useful for anything checked into Git.
json.dumps({"city": "पुणे"}, ensure_ascii=False)
By default, non-ASCII characters are escaped — "पुणे".
Valid, and unreadable. ensure_ascii=False writes the actual characters, which
is what you want for any language other than English. Pair it with
encoding="utf-8" on the file.
How types map
| JSON | Python |
|---|---|
| object | dict |
| array | list |
| string | str |
| number | int or float |
true / false |
True / False |
null |
None |
Mostly unsurprising. The traps are what is missing.
What JSON cannot hold
json.dumps({"when": datetime.now()})
TypeError: Object of type datetime is not JSON serializable
JSON has no date type. Nor sets, nor tuples-as-tuples, nor your own classes.
Dates must be converted, and the sane choice is ISO 8601:
from datetime import datetime
data = {"when": datetime.now().isoformat()}
# '2026-09-27T14:30:00.123456'
back = datetime.fromisoformat(data["when"])
ISO format sorts correctly as a string, which is a genuine convenience.
Sets become lists:
data = {"tags": list(my_set)}
And come back as lists, so convert again on the way in if you need a set.
Tuples are silently converted to arrays, and come back as lists. This one is worth noticing:
original = {"point": (10, 20)}
restored = json.loads(json.dumps(original))
print(restored["point"]) # [10, 20]
print(type(restored["point"])) # <class 'list'>
No error, no warning — a quiet type change. If code downstream expects a tuple, or uses it as a dictionary key, it will fail somewhere far away.
Dictionary keys become strings, always:
original = {1: "one", 2: "two"}
restored = json.loads(json.dumps(original))
print(restored) # {'1': 'one', '2': 'two'}
Integer keys go in and string keys come out. This bites people caching data keyed by id.
The rule: a round trip through JSON is not guaranteed to give you back exactly what you put in. Know which of your types survive.
Handling bad JSON
try:
with open("config.json", encoding="utf-8") as file:
config = json.load(file)
except FileNotFoundError:
print("No config file; using defaults.")
config = {}
except json.JSONDecodeError as error:
print(f"config.json is not valid JSON: {error}")
config = {}
JSONDecodeError gives the line and column, which usually locates a missing
comma immediately.
Both cases are worth handling separately: a missing file is normal, a corrupt one is a problem somebody should hear about.
Common causes of invalid JSON, all of which are legal Python and not JSON:
- Trailing commas —
{"a": 1,} - Single quotes —
{'a': 1} - Comments — JSON has none
None,True,Falseinstead ofnull,true,false
That last one catches people who build JSON by string formatting. Do not do
that — use json.dumps, which handles quoting and escaping correctly. Hand-built
JSON breaks the moment a value contains a quote or a newline.
A practical example
Reading an API-shaped response, from module 4's nesting lesson:
import json
with open("orders.json", encoding="utf-8") as file:
response = json.load(file)
totals = {}
for user in response.get("data", {}).get("users", []):
total = sum(order["qty"] * order["price"] for order in user.get("orders", []))
totals[user["name"]] = total
with open("totals.json", "w", encoding="utf-8") as file:
json.dump(totals, file, indent=2, ensure_ascii=False)
.get() with defaults at each level means a missing data or users key gives
an empty result rather than a crash — the defensive habit from module 4, now
protecting a real file read.
Practice
- Write a dictionary to
data.jsonwithindent=2. Open it in your editor. - Read it back and confirm you get the same values.
- Write it without
indentand compare the files. - Parse a JSON string with
loads. Then tryloadon it and read the error. - Save a dictionary containing a
datetime. Read theTypeError, then fix it with.isoformat()and convert it back on reading. - Round-trip a dictionary containing a tuple and an integer key. Print the types before and after and explain both changes.
- Save text containing
पुणेor₹with and withoutensure_ascii=False. - Hand-write an invalid JSON file — a trailing comma — and handle the
JSONDecodeError, printing the line number. - Write a small program that loads a JSON config, applies defaults for missing keys, and saves it back.
- Build a JSON string by string formatting, with a value containing a double
quote. Watch it break. Then use
json.dumps.
Next: CSV, which looks simpler than JSON and is not.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship