Structuring Your Data

Reading and Writing JSON


Learning Objectives

  • You can explain why JSON fits named, nested data better than CSV.
  • You can write data to a file with json.dump and read it back with json.load.
  • You can describe which Python types survive a round trip through JSON.
  • You can update the study log to store its sessions as JSON.

The sessions are dictionaries in memory now, but they are still saved as CSV. CSV is a good fit when a session was a flat row with a set of columns. Now that the session is a collection of named fields, the storage format can be adjusted to match. The natural choice is JSON, which is designed to represent exactly the kind of nested, named data the study log has become.

The moment a record contains a list, or a nested record, CSV becomes a poor fit for storing the structure. We could, still, write the study log sessions as CSV if we wanted — we would just have to flatten the dictionaries into rows, and then parse them back out again when we read.

JavaScript Object Notation

JSON — JavaScript Object Notation, though it has nothing to do with JavaScript in practice — is a text-based format for data exchange, including storing data. Its building blocks line up almost one-to-one with Python’s:

  • a JSON object is written {"key": value, ...} and corresponds to a Python dictionary,
  • a JSON array is written [value, value, ...] and corresponds to a Python list,
  • strings, numbers, true/false, and null correspond to Python strings, numbers, True/False, and None.

Here is the study log’s sessions as JSON:

[
{"subject": "math", "minutes": 45, "date": "2024-03-10"},
{"subject": "physics", "minutes": 90, "date": "2024-03-10"},
{"subject": "history", "minutes": 30, "date": "2024-03-11"}
]

That looks almost exactly like the Python list-of-dicts from the previous chapter. JSON is a text rendering of the same nested structure.

Writing JSON

Python’s json module converts between Python objects and JSON text. To write a Python object to a JSON file, you use json.dump, passing the data and an open file:

import json
sessions = [
{"subject": "math", "minutes": 45, "date": "2024-03-10"},
{"subject": "physics", "minutes": 90, "date": "2024-03-10"},
]
with open("sessions.json", "w", encoding="utf-8") as f:
json.dump(sessions, f, indent=2)

The call json.dump(sessions, f, indent=2) writes the whole structure to the file in one call. The indent=2 argument is optional; it pretty-prints the JSON with two-space indentation so a human can read the file. Without it, json.dump writes everything on one line, which is valid but harder to inspect.

Compare this to the CSV writer, which had to open the file, write a header row, then loop over the sessions writing each one as a row. With JSON there is no header and no loop — the structure of the data carries its own field names, and one call serialises all of it.

Reading JSON

To read, you use json.load, passing an open file. It returns the Python object the JSON describes:

import json
with open("sessions.json", encoding="utf-8") as f:
sessions = json.load(f)
print(sessions[0]["subject"]) # math

The call json.load reads the entire file and reconstructs the list of dictionaries. There is no per-row parsing, no int() conversion, no header to skip. The minutes come back as numbers because they were stored as numbers; the structure comes back as a list of dictionaries because that is what it was.

Round Trip and Types

JSON has a fixed, small set of types: objects (dictionaries), arrays (lists), strings, numbers, booleans, and null. Anything you store has to be made of those. Strings, numbers, lists, dictionaries, True, False, and None all round-trip cleanly — write them and read them back and you get the same value.

Types outside that set do not survive, because JSON has no way to write them. A Python date object, for instance, cannot be stored directly:

from datetime import date
json.dump({"day": date.today()}, f)
# TypeError: Object of type date is not JSON serializable

This is why dates are stored as ISO strings like "2024-03-10" rather than as date objects — a string is something JSON can hold, and the summary functions parse it back into a date with date.fromisoformat when they need to do date arithmetic.

Keeping the stored form to JSON’s basic types, and converting to richer types only in memory, is a pattern.

Round-trip items through JSON

0 / 10 points

Write a function named round_trip_items(path, items).

The argument items is a list of dictionaries containing only JSON-friendly values. The function must:

  1. write items to path as JSON,
  2. read the same file back,
  3. and return the loaded value.

Requirements:

  • use Python’s json module,
  • keep numbers as numbers and strings as strings,
  • do not change the input list or its dictionaries,
  • do not print anything,
  • and do not use input(...).

Updating the Study Log

With json.dump and json.load, the study log’s save and load become short. Saving:

import json
def save_sessions(path, sessions):
"""Write the sessions to a JSON file."""
with open(path, "w", encoding="utf-8") as f:
json.dump(sessions, f, indent=2)

Loading needs to keep the first-run behaviour — a missing file means an empty log, not a crash — and otherwise hands the work to json.load:

import json
def load_sessions(path):
"""Load sessions from a JSON file. Returns [] for a missing file."""
try:
f = open(path, encoding="utf-8")
except FileNotFoundError:
return []
with f:
return json.load(f)

This is considerably shorter than the CSV loader, and it raises a fair question: where did all the validation that is part of defensive programming go — the bad-minutes check, the bad-date check, the future-date rejection?

The CSV loader had to validate heavily because CSV values are all bare strings, and a hand-edited CSV could put anything in any column. JSON guarantees syntactically valid JSON and preserves basic value types, but the loader may still need to validate that the data has the expected study-log shape.

The validation that remains worth doing — rejecting a future date, say, or confirming the minutes are sensible — is a judgment call. For now, the point is that moving to JSON removed a whole category of parsing and validation that CSV forced.

However, there is an important caveat. The JSON loader assumes that the file, if it exists, is valid JSON. If the file is present but contains invalid JSON — perhaps because it was hand-edited — json.load raises a JSONDecodeError. This is a different kind of error than a missing file, and it is reasonable to let it surface rather than silently starting over with an empty log and discarding the user’s data.

Study log JSON storage

0 / 40 points

Return to your study log dashboard from records and refactor its storage from CSV to JSON.

This checkpoint builds on top of the exercise Study log dashboard from records from the previous chapter. The in-memory representation stays the same: sessions are still dictionaries with named fields.

{"subject": "math", "minutes": 45, "date": "2026-05-10"}

The file format changes. Store the data in sessions.json as a JSON list of dictionaries:

[
{"subject": "math", "minutes": 45, "date": "2026-05-10"},
{"subject": "history", "minutes": 30, "date": "2026-05-11"}
]

Required helpers:

FunctionRequired result
classify_session(minutes)"short" below 15, "solid" from 15 through 90, "long" above 90
total_minutes(minutes_list)total minutes, or 0 for an empty list
longest_session(minutes_list)largest minute value, or None for an empty list
get_session()ask for subject and minutes, returning (None, None) for done
get_minutes(sessions)extract the "minutes" values from session dictionaries
load_sessions(path)load and return the JSON list of session dictionaries
save_sessions(path, sessions)write the session dictionaries as JSON
summaries_by_subject(sessions)sorted [subject, count, total, average] rows
sessions_between(sessions, start, end)original session dictionaries inside the inclusive date window
totals_by_day(sessions)sorted [date_string, total_minutes] rows
heaviest_day(sessions)earliest [date_string, total_minutes] row with the largest total, or None
recent_daily_totals(sessions, today=None)totals for today and the previous six days
distinct_days(sessions)sorted distinct date objects
longest_streak(sessions)longest run of consecutive session days, or 0
current_streak(sessions, today=None)current streak; yesterday still counts, old streaks return 0, future dates raise ValueError
print_summary(sessions, today=None)print the opening dashboard using session dictionaries
main()run the complete study log program

JSON storage requirements:

  • define SESSIONS_FILE = "sessions.json",
  • use Python’s json module,
  • use json.dump(sessions, f, indent=2) in save_sessions(path, sessions),
  • open files with encoding="utf-8",
  • return [] from load_sessions(path) when the file does not exist,
  • let invalid JSON raise json.JSONDecodeError,
  • do not use the csv module in this checkpoint,
  • and do not parse files with .split(",").

The dashboard behavior should stay the same as in the records checkpoint. For these sessions and today set to May 12, 2026:

sessions = [
{"subject": "math", "minutes": 45, "date": "2026-05-10"},
{"subject": "history", "minutes": 30, "date": "2026-05-10"},
{"subject": "math", "minutes": 60, "date": "2026-05-11"},
{"subject": "physics", "minutes": 90, "date": "2026-05-12"},
]

print_summary(sessions, today=date(2026, 5, 12)) should print:

So far: 4 sessions, 3 hours 45 minutes
By subject:
History: 1 session, 30 minutes, average 30.0 minutes
Math: 2 sessions, 105 minutes, average 52.5 minutes
Physics: 1 session, 90 minutes, average 90.0 minutes
Recent days:
2026-05-10: 75 minutes
2026-05-11: 60 minutes
2026-05-12: 90 minutes
Heaviest day: 2026-05-12 (90 minutes)
Current streak: 3 days
Longest streak: 3 days

Complete program behavior:

  • do not ask for input when the file is imported,
  • guard the input loop with if __name__ == "__main__": main(),
  • print === Study Log === at startup,
  • load existing JSON sessions before asking for input,
  • call print_summary(sessions) before asking for a new session,
  • keep asking for subjects until the user types done,
  • accept variants such as Done, DONE, and done ,
  • ask for minutes only after a non-quit subject,
  • re-prompt with Please enter a whole number of minutes. after non-numeric minutes,
  • create a new session dictionary for each recorded session,
  • save immediately after every successfully recorded session,
  • keep the response lines for short, solid, and long sessions,
  • and print the end-of-run === Summary ===.

Check Your Understanding

  1. What are the main differences between storing data as CSV and storing it as JSON? When is one a better fit than the other?
  2. Which kinds of Python values survive a round trip through JSON, and which do not?
  3. Why does the study log store dates as strings like "2024-03-10" rather than as date objects?
  4. The JSON loader is much shorter than the earlier CSV loader. Why did so much of the validation become unnecessary?
  5. How should the loader treat a missing file, and how should it treat a corrupt one? Why the difference?

AI Study Aid

Create a chapter diagram

We're looking into to what extent AI-generated diagrams could help with studying.

Use this study aid to generate an AI-generated visual summary of the material you just studied. Each diagram style emphasizes the content in a different way, so you can choose the focus that feels most useful.

Using the diagram generator is voluntary and does not affect your course progress. We encourage you to try it out and see if it helps, but it's totally up to you! Your answers help us understand how to make better study aids in the future.

Diagrams tried: 0