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.dumpand read it back withjson.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, andnullcorrespond to Python strings, numbers,True/False, andNone.
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"]) # mathThe 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 serializableThis 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:
- write
itemstopathas JSON, - read the same file back,
- and return the loaded value.
Requirements:
- use Python’s
jsonmodule, - 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:
| Function | Required 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
jsonmodule, - use
json.dump(sessions, f, indent=2)insave_sessions(path, sessions), - open files with
encoding="utf-8", - return
[]fromload_sessions(path)when the file does not exist, - let invalid JSON raise
json.JSONDecodeError, - do not use the
csvmodule 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 daysLongest streak: 3 daysComplete 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, anddone, - 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
- What are the main differences between storing data as CSV and storing it as JSON? When is one a better fit than the other?
- Which kinds of Python values survive a round trip through JSON, and which do not?
- Why does the study log store dates as strings like
"2024-03-10"rather than asdateobjects? - The JSON loader is much shorter than the earlier CSV loader. Why did so much of the validation become unnecessary?
- 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.