RizTech Academy logo
RizTech Academy
Files and DataLesson 3 of 525 min

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, False instead of null, 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

  1. Write a dictionary to data.json with indent=2. Open it in your editor.
  2. Read it back and confirm you get the same values.
  3. Write it without indent and compare the files.
  4. Parse a JSON string with loads. Then try load on it and read the error.
  5. Save a dictionary containing a datetime. Read the TypeError, then fix it with .isoformat() and convert it back on reading.
  6. Round-trip a dictionary containing a tuple and an integer key. Print the types before and after and explain both changes.
  7. Save text containing पुणे or ₹ with and without ensure_ascii=False.
  8. Hand-write an invalid JSON file — a trailing comma — and handle the JSONDecodeError, printing the line number.
  9. Write a small program that loads a JSON config, applies defaults for missing keys, and saves it back.
  10. 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