Working with JSON Files
We give the repository its first genuinely external data source: a real JSON file on disk, read and parsed with Python's standard library.
Text on disk, parsed into the data structures you already know.
1. Learning Objective
By the end of this session you will be able to:
- Write Python data to a JSON file with the json module
- Read and parse a JSON file back into Python data structures
- Handle a missing or malformed JSON file gracefully, reusing Session 11's error handling
- Point CountryRepository at a JSON file instead of the in-memory mock list
- Explain the relationship between JSON types and Python types
2. Pre-Coding Quiz
Answer these before reading the concept explanation or writing any code. It is fine to get these wrong โ that is the point. You need 4/5 to proceed to the lab.
What does json.dump(MOCK_COUNTRIES, f) do, given an open file f?
json.dump(data, file) serializes a Python data structure (here, a list of dicts) into JSON text and writes it to the given open file.After reading a JSON file with data = json.load(f), what Python type does a JSON array of objects become?
What happens if you call json.load(f) on a file that does not exist?
FileNotFoundError at the open() call itself โ this needs a try/except (Session 11) if the file might legitimately be missing.What happens if the file exists but contains invalid JSON text, e.g. a typo'd bracket?
json.JSONDecodeError (a subclass of ValueError) โ another case where try/except lets you fail gracefully with a clear message instead of crashing the whole program.To point CountryRepository at a JSON file instead of MOCK_COUNTRIES, what needs to change, given Session 28's design?
3. The Concept โ Reading and Writing JSON
JSON arrays become Python lists; JSON objects become Python dicts โ the same shape we've worked with since Session 06.
Writing data to a JSON file
Python's standard library json module converts Python data structures to and from JSON text โ no external installation required.
import json
from mock_countries import MOCK_COUNTRIES
with open("countries.json", "w") as f:
json.dump(MOCK_COUNTRIES, f, indent=2)
# countries.json now contains readable, formatted JSON text on disk
Reading it back
json.load() parses a JSON file directly back into native Python data structures โ a JSON array of objects becomes exactly the list of dicts we've used since Session 06.
import json
with open("countries.json") as f:
data = json.load(f)
print(type(data)) # <class 'list'>
print(type(data[0])) # <class 'dict'>
print(data[0]["name"]) # "Kenya"
Handling a missing or malformed file
A file that does not exist, or contains invalid JSON, raises an exception โ Session 11's discipline applies directly.
import json
def load_countries_file(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError:
print(f"{path} does not exist โ using an empty dataset")
return []
except json.JSONDecodeError as e:
print(f"{path} contains invalid JSON: {e}")
return []
data = load_countries_file("countries.json")
data_missing = load_countries_file("does_not_exist.json") # handled gracefully
Pointing the repository at the file โ nothing else changes
This is Session 28's payoff, delivered: only the constructor argument changes, because get_all() was never coupled to WHERE raw_data came from.
class CountryRepository:
def __init__(self, raw_data):
self._raw_data = raw_data
def get_all(self):
return [Country.from_dict(r) for r in self._raw_data]
# Before: built from the in-memory mock list
# repo = CountryRepository(raw_data=MOCK_COUNTRIES)
# Now: built from a real file on disk โ get_all() is completely unchanged
repo = CountryRepository(raw_data=load_countries_file("countries.json"))
print(len(repo.get_all()))
4. Lab
What you will build
A file called json_lab.py, plus a generated countries.json.
Step-by-step instructions
Create the file and write the mock data to countries.json
# json_lab.py
import json
from mock_countries import MOCK_COUNTRIES
with open("countries.json", "w") as f:
json.dump(MOCK_COUNTRIES, f, indent=2)
print("Wrote countries.json")
Open countries.json in a text editor and confirm it is readable JSON
No code needed โ just look at the file to see what json.dump produced.
Write load_countries_file() with graceful error handling
def load_countries_file(path):
try:
with open(path) as f:
return json.load(f)
except FileNotFoundError:
print(f"{path} does not exist โ using an empty dataset")
return []
except json.JSONDecodeError as e:
print(f"{path} contains invalid JSON: {e}")
return []
Load the real file and a nonexistent file, confirming both are handled gracefully
real_data = load_countries_file("countries.json")
missing_data = load_countries_file("does_not_exist.json")
print("Real records:", len(real_data))
print("Missing-file fallback:", missing_data)
Point CountryRepository at the JSON file and confirm everything still works
class Country:
def __init__(self, name, region, population):
self.name = name
self.region = region
self.population = population
@classmethod
def from_dict(cls, data):
return cls(**data)
class CountryRepository:
def __init__(self, raw_data):
self._raw_data = raw_data
def get_all(self):
return [Country.from_dict(r) for r in self._raw_data]
repo = CountryRepository(raw_data=load_countries_file("countries.json"))
print(len(repo.get_all()))
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
json_lab.py |
Created | Writes and reads countries.json, with graceful error handling, feeding CountryRepository. |
countries.json |
Generated | The mock data, now persisted as a real file on disk. |
docs/sessions/session-29/index.html |
Created | This session document. |
6. Commit Checkpoint
Once the lab is complete and you can explain every line, make this exact commit:
git add json_lab.py countries.json docs/sessions/session-29/index.html
git commit -m "session-29: read and write JSON files, point the repository at real disk data"
7. Code Review Checklist
Go through your code line by line and check each item:
8. Post-Coding Quiz
Same 5 questions. Take it again now that you have written and run the code. You need 4/5 to mark this session complete.
What does json.dump(MOCK_COUNTRIES, f) do, given an open file f?
json.dump(data, file) serializes a Python data structure (here, a list of dicts) into JSON text and writes it to the given open file.After reading a JSON file with data = json.load(f), what Python type does a JSON array of objects become?
What happens if you call json.load(f) on a file that does not exist?
FileNotFoundError at the open() call itself โ this needs a try/except (Session 11) if the file might legitimately be missing.What happens if the file exists but contains invalid JSON text, e.g. a typo'd bracket?
json.JSONDecodeError (a subclass of ValueError) โ another case where try/except lets you fail gracefully with a clear message instead of crashing the whole program.To point CountryRepository at a JSON file instead of MOCK_COUNTRIES, what needs to change, given Session 28's design?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does json.load() raise a FileNotFoundError from the open() call rather than from json.load() itself? What does that tell you about the order operations happen in?
- What is the practical difference for a user between your program silently falling back to an empty list versus crashing entirely, when countries.json is missing?
- Manually break countries.json (delete a bracket) and confirm your JSONDecodeError handling catches it. What did the error message tell you?
- How does this session prove Session 28's repository design was worth the extra structure, now that a genuinely different data source exists?
10. What Breaks If This Knowledge Is Missing?
- Crash on missing or corrupt files: Without explicit handling for FileNotFoundError and JSONDecodeError, a single missing file or one bad edit to countries.json would crash the entire application on startup instead of degrading gracefully.
- Data contracts (Session 30): Nothing currently verifies that every record read from the JSON file actually has the right keys and types โ a JSON file is just text, and can contain anything. The next session formalizes exactly this check.
- Real APIs (Session 42): Reading and parsing JSON is exactly what a real API response requires too โ this session's json.load() pattern is nearly identical to how you will parse an HTTP response body in Layer 7.
11. What We Learned
Python concept mastered: Reading and writing JSON files with the json module, handling missing/malformed files gracefully, and pointing a repository at a real disk-based data source.
Unlocks: The application now has a genuinely persistent, external data source โ the first real (if still local) data the repository pattern was built to support.
Next session: Session 30 โ Designing Data Contracts. We formalize what a "valid" country record actually looks like, using type hints and dataclasses โ the Layer 4 gate.