Layer 4 Session 29 Mock Data

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.

"name": "Kenya" countries.json

Text on disk, parsed into the data structures you already know.

Estimated time: 35โ€“40 minutes  ยท  Pre-quiz โ†’ Concept โ†’ Lab โ†’ Commit โ†’ Post-quiz

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.

Question 1 of 5

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.
Question 2 of 5

After reading a JSON file with data = json.load(f), what Python type does a JSON array of objects become?

A JSON array becomes a Python list; a JSON object becomes a Python dict. A JSON array of objects โ€” exactly our country records โ€” becomes a list of dicts, the exact shape we have been using since Session 06.
Question 3 of 5

What happens if you call json.load(f) on a file that does not exist?

Attempting to open a nonexistent file for reading raises FileNotFoundError at the open() call itself โ€” this needs a try/except (Session 11) if the file might legitimately be missing.
Question 4 of 5

What happens if the file exists but contains invalid JSON text, e.g. a typo'd bracket?

Malformed JSON raises 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.
Question 5 of 5

To point CountryRepository at a JSON file instead of MOCK_COUNTRIES, what needs to change, given Session 28's design?

This is exactly the payoff promised in Session 28: as long as the JSON file's parsed content is still a list of dicts with the same keys, only the CONSTRUCTOR argument changes โ€” the repository's methods and everything downstream of it are completely untouched.

3. The Concept โ€” Reading and Writing JSON

JSON ARRAY[ {...}, {...} ]PYTHONlist of dicts

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

Lab objective: Write the mock data to a real JSON file, read it back with graceful error handling, and point CountryRepository at it.

What you will build

A file called json_lab.py, plus a generated countries.json.

Step-by-step instructions

1

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")
2

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.

3

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 []
4

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)
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 3 files.

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"
Do not commit until you can answer out loud: "What two things could go wrong when reading countries.json, and how did I handle each one?"

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.

Question 1 of 5

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.
Question 2 of 5

After reading a JSON file with data = json.load(f), what Python type does a JSON array of objects become?

A JSON array becomes a Python list; a JSON object becomes a Python dict. A JSON array of objects โ€” exactly our country records โ€” becomes a list of dicts, the exact shape we have been using since Session 06.
Question 3 of 5

What happens if you call json.load(f) on a file that does not exist?

Attempting to open a nonexistent file for reading raises FileNotFoundError at the open() call itself โ€” this needs a try/except (Session 11) if the file might legitimately be missing.
Question 4 of 5

What happens if the file exists but contains invalid JSON text, e.g. a typo'd bracket?

Malformed JSON raises 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.
Question 5 of 5

To point CountryRepository at a JSON file instead of MOCK_COUNTRIES, what needs to change, given Session 28's design?

This is exactly the payoff promised in Session 28: as long as the JSON file's parsed content is still a list of dicts with the same keys, only the CONSTRUCTOR argument changes โ€” the repository's methods and everything downstream of it are completely untouched.

9. Reflection Questions

Think through these after the post-quiz. No right answer โ€” they are for discussion.

  1. 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?
  2. 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?
  3. Manually break countries.json (delete a bracket) and confirm your JSONDecodeError handling catches it. What did the error message tell you?
  4. 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.