Skip to content
aviral gupta

// B5.4 · ~25 min · Beginner

Saving structured data with JSON

After this lesson you can save lists and dictionaries to a JSON file and load them back, and predict what changes on the way: tuples, number keys and sets.

Lesson 4 of 5 in B5 Modules, files and errors

You will be able to

  • Turn Python data into JSON text and back with json.dumps and json.loads, pretty-printed with indent
  • Save data to a file and load it again with json.dump and json.load
  • Predict what JSON changes or refuses: tuples become lists, keys become strings, sets fail
  1. Warm-up · Activity 1 of 7

    Warm-up from the last lesson: which statements about files are true? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: what does this print?

    import json
    
    print(json.dumps({"name": "Ada", "active": True, "score": None}))
  3. Practice · Activity 3 of 7

    Fill in the function that turns the JSON text into Python data.

    import json
    
    text = '{"x": 1, "y": [2, 3]}'
    data = json.____(text)
    print(data["y"][1])
    json.(text)
  4. Practice · Activity 4 of 7

    A dict with number keys goes to JSON and back. What does this print?

    import json
    
    print(json.loads(json.dumps({1: "a", 2: "b"})))
  5. Practice · Activity 5 of 7

    Match each Python value to what json.dumps writes for it.

  6. Brain teaser · Activity 6 of 7

    Brain teaser. A tuple goes to JSON and back. What does this print?

    import json
    
    point = (3, 4)
    back = json.loads(json.dumps(point))
    print(back == point, back)
  7. Apply · Activity 7 of 7

    Mini-task. Save settings = {"theme": "dark", "font_size": 14} to settings.json with indent=2. Then load the file, add 2 to font_size, save it again, and load and print it once more. Open the file in the editor, or print its text, to see the indentation.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

An address book in a JSON file

A list of contact dicts is saved with json.dump and indent=2, the raw file is printed so you can see real JSON, and the file is loaded back with json.load. The last line uses json.dumps, which returns the text instead of writing it.

main.py

import json

contacts = [
    {"name": "Ada", "phone": "+49 30 1234", "tags": ["work"]},
    {"name": "Grace", "phone": None, "tags": []},
]

with open("contacts.json", "w", encoding="utf-8") as f:
    json.dump(contacts, f, indent=2)

with open("contacts.json", encoding="utf-8") as f:
    print(f.read())

with open("contacts.json", encoding="utf-8") as f:
    loaded = json.load(f)

print(loaded == contacts, loaded[1]["phone"])
print(json.dumps({"total": 2, "ok": True}))

Run it with

python main.py

Output

[
  {
    "name": "Ada",
    "phone": "+49 30 1234",
    "tags": [
      "work"
    ]
  },
  {
    "name": "Grace",
    "phone": null,
    "tags": []
  }
]
True None
{"total": 2, "ok": true}
  • indent=2 puts every item on its own line, two more spaces per level; an empty list stays [].
  • Python’s None is stored as null and comes back as None.
  • loaded == contacts is True: lists, dicts, strings and None all survive the round trip unchanged.
  • Without indent, dumps writes everything on one line.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Save and load any data

Write save_json(path, data), which writes data to the file as JSON indented with 2 spaces, replacing the old content, and load_json(path), which reads the file and returns the data. Use json.dump and json.load, and open the file with encoding="utf-8" both times.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    json.dump(data, f) writes into an open file; json.load(f) reads from one.

  2. Hint 2

    Pass indent=2 to json.dump to get one item per line with two spaces per level.

  3. Hint 3

    In save_json: json.dump(data, f, indent=2). In load_json: return json.load(f).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import json


def save_json(path: str, data: object) -> None:
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2)


def load_json(path: str) -> object:
    with open(path, encoding="utf-8") as f:
        return json.load(f)


if __name__ == "__main__":
    save_json("demo.json", {"tasks": ["write lesson"], "done": False})
    print(load_json("demo.json"))
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

import json


def save_json(path: str, data: object) -> None:
    with open(path, "w", encoding="utf-8") as f:
        f.write(str(data))


def load_json(path: str) -> object:
    with open(path, encoding="utf-8") as f:
        return json.loads(f.read())


if __name__ == "__main__":
    save_json("demo.json", {"tasks": ["write lesson"], "done": False})
    print(load_json("demo.json"))

test_main.py

import json

from main import load_json, save_json


def test_round_trip():
    """A dict with a list, None and True comes back equal"""
    data = {"tasks": ["a", "b"], "due": None, "done": True, "city": "München"}
    save_json("t1.json", data)
    got = load_json("t1.json")
    assert got == data, f"load_json returned {got!r}, expected {data!r}"


def test_real_json():
    """The file holds JSON that other programs can read"""
    save_json("t2.json", {"done": False})
    with open("t2.json", encoding="utf-8") as f:
        text = f.read()
    try:
        json.loads(text)
    except ValueError:
        assert False, f"the file is not valid JSON: {text!r}"
    assert "false" in text, f"the file holds {text!r}: False must be written as false"


def test_indent():
    """The file is indented with 2 spaces"""
    save_json("t3.json", {"a": 1})
    with open("t3.json", encoding="utf-8") as f:
        text = f.read()
    assert text == '{\n  "a": 1\n}', f"the file holds {text!r}, expected 2-space indentation"

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Exercise 2 of 2

Who lives in this city?

contacts.json (second tab) holds a list of contacts, each a dict with name and city. Read a city with input(), load the file with json.load, and print the names of everyone in that city in alphabetical order, one per line. If nobody lives there, print nobody in and the city, for example nobody in Rome.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads Python for your browser (up to 6.5 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    with open("contacts.json", encoding="utf-8") as f: contacts = json.load(f) gives you a list of dicts.

  2. Hint 2

    Each contact is a dict, so contact["city"] and contact["name"] read its fields.

  3. Hint 3

    sorted(contact["name"] for contact in contacts if contact["city"] == city) gives the names in order; an empty list is false.

Show a solution

One way to solve it. Yours can look different and still pass the checks.

import json

city = input()
with open("contacts.json", encoding="utf-8") as f:
    contacts = json.load(f)

names = sorted(contact["name"] for contact in contacts if contact["city"] == city)
if names:
    for name in names:
        print(name)
else:
    print("nobody in", city)
Run it on your computer

Install Python 3.14 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.py

import json

city = input()
# Load contacts.json and print the names of the contacts in this city.
print("nobody in", city)

test_main.py

from learnrun import run_main


def names_in(city):
    return run_main(city + "\n").strip().splitlines()


def test_berlin():
    """Berlin lists Ada and Linus, sorted"""
    got = names_in("Berlin")
    assert got == ["Ada", "Linus"], f"for Berlin the program printed {got!r}"


def test_munich():
    """München lists one name"""
    got = names_in("München")
    assert got == ["Grace"], f"for München the program printed {got!r}"


def test_nobody():
    """A city with no contacts prints nobody in …"""
    got = names_in("Rome")
    assert got == ["nobody in Rome"], f"for Rome the program printed {got!r}"

contacts.json

[
  {"name": "Linus", "city": "Berlin"},
  {"name": "Grace", "city": "München"},
  {"name": "Ada", "city": "Berlin"}
]

On macOS and Linux, type python3 wherever these commands say python, as in the first lesson.

Run the program:

python main.py

Run the checks (needs learnrun.py in the same folder):

python learnrun.py test
Download learnrun.py

Common mistakes

Saving a set

import json

tags = {"work", "home"}
print(json.dumps(tags))

What Python prints

TypeError: Object of type set is not JSON serializable

Why, and the fix

JSON has no sets. Convert the set to a list before saving, ideally sorted so the file is the same every time: json.dumps(sorted(tags)). After loading, set(...) turns it back into a set.

Writing JSON with single quotes

import json

data = json.loads("{'name': 'Ada'}")

What Python prints

json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)

Why, and the fix

JSON strings and keys need double quotes; single quotes are Python, not JSON. Write the text as '{"name": "Ada"}', or better, let json.dumps produce it instead of typing JSON by hand.

Passing a file name to json.load

import json

data = json.load("data.json")

What Python prints

AttributeError: 'str' object has no attribute 'read'

Why, and the fix

json.load expects an open file, and it calls .read() on whatever you pass. Open the file first: with open("data.json", encoding="utf-8") as f: data = json.load(f). json.loads is the one that takes a string, of JSON text, not a file name.

Python in the browser: Pyodide 314.0.7, MPL-2.0. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Text in, text out: dumps and loads

JSON is a text format for data that almost every language can read. json.dumps(data) turns Python data into a JSON string, and json.loads(text) turns a JSON string back into Python data. The text looks like Python, with differences: strings always use double quotes, True and False become true and false, and None becomes null. json.dumps(data, indent=2) spreads the text over several lines with two spaces per level, which is easier to read and to compare.

Files: dump and load

json.dump(data, f) writes the JSON text straight into a file opened for writing, and json.load(f) reads a whole file opened for reading and returns the data. Mind the names: without the s they take a file object, with the s they take or return a string. Open JSON files with encoding="utf-8" in both directions. The usual pattern is to load the file, change the list or dict in memory, and dump it again with "w".

What JSON can hold

JSON knows objects, arrays, strings, numbers, true, false and null. So dicts, lists, tuples, strings, ints, floats, True, False and None can be saved, and nothing else. A tuple is saved as an array and comes back as a list. Dict keys are always strings in JSON, so {1: "a"} comes back as {"1": "a"}. A set, or an object of your own class, raises TypeError: Object of type set is not JSON serializable. Convert first, for example with sorted(my_set).

Sources

Last reviewed September 29, 2026