Layer 4 Session 28 Mock Data

Building a Data Access Layer

Application logic should not know or care where its data comes from. We build a CountryRepository that hides that detail behind a clean, swappable interface.

get_all() Country

A single, swappable gateway between the app and its data.

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:

  • Build a repository class that wraps a data source behind a small set of methods
  • Explain the separation of concerns between "fetching/storing data" and "using data"
  • Swap a repository's underlying data source without changing any code that calls it
  • Explain why this separation makes the application easier to test later (Layer 5 preview)
  • Distinguish a repository's methods from CountryExplorer's methods from Session 16

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

A CountryRepository class wraps MOCK_COUNTRIES and exposes get_all() and find_by_region(region). Why not just let every part of the application import MOCK_COUNTRIES directly?

This is separation of concerns: the repository owns the detail of HOW data is fetched (a mock list today, a JSON file or real API tomorrow); everything else only needs to know WHAT methods the repository offers, not how they work internally.
Question 2 of 5

If CountryRepository.get_all() currently reads from MOCK_COUNTRIES, and later you change it to read from a JSON file instead, what code outside the repository needs to change?

This is the entire point of the abstraction: as long as get_all() keeps returning the same shape of data, its INTERNAL implementation can change freely (mock list, JSON file, real API) without touching a single line of code anywhere else.
Question 3 of 5

What is the difference between CountryRepository (this session) and CountryExplorer (Session 16)?

These are two different responsibilities: the repository knows how to get data; the explorer knows how to work with a set of already-fetched Country instances. Keeping them separate (rather than one giant class doing both) is exactly the kind of focused-class discipline from Session 16.
Question 4 of 5

Why does this separation make future testing (Layer 5) easier?

Because the repository is a clean, swappable interface, a test can construct one around exactly the tiny, controlled dataset it needs for that specific test โ€” this is precisely what Session 35 (testing the data layer) will do.
Question 5 of 5

Which method belongs on CountryRepository rather than CountryExplorer, given the separation of concerns described in this session?

get_all() is about FETCHING data from its source โ€” the repository's job. The others are about operating on an already-fetched, in-memory working set โ€” the explorer's job, as established in Session 16 and 22.

3. The Concept โ€” A Data Access Layer

APPLICATION CODErepo.get_all()COUNTRYREPOSITORYhides the sourceSOURCE (SWAPPABLE)mock, JSON,real API...

Application code depends only on the repository's methods โ€” never directly on where the data actually lives.

Wrapping a data source behind an interface

A repository is a small class whose only job is providing access to data โ€” hiding exactly where and how that data is stored behind a clean set of methods.

from mock_countries import MOCK_COUNTRIES

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]

    def find_by_region(self, region):
        return [Country.from_dict(r) for r in self._raw_data if r["region"] == region]

repo = CountryRepository(raw_data=MOCK_COUNTRIES)
all_countries = repo.get_all()
print(len(all_countries))

Swapping the source without touching callers

As long as get_all() keeps returning the same shape (a list of Country instances), the repository's internals can change completely, and nothing that calls repo.get_all() needs to know or care.

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]

# Today: built around mock data
repo = CountryRepository(raw_data=MOCK_COUNTRIES)

# Later (Session 29/42): built around a completely different source โ€”
# but repo.get_all() everywhere else in the app doesn't change at all
# repo = CountryRepository(raw_data=load_from_json_file("countries.json"))
# repo = CountryRepository(raw_data=fetch_from_real_api())

Repository vs Explorer โ€” two different responsibilities

CountryRepository fetches raw data and hands back Country instances. CountryExplorer (Session 16) takes an already-fetched working set and offers operations over it. Keeping these responsibilities separate mirrors Session 16's "small, focused classes" principle.

repo = CountryRepository(raw_data=MOCK_COUNTRIES)
explorer = CountryExplorer(countries=repo.get_all())  # repository FETCHES, explorer OPERATES

print(explorer.total_population)         # explorer's job
print(repo.find_by_region("Africa"))     # repository's job โ€” a different kind of query

4. Lab

Lab objective: Build a CountryRepository wrapping the mock data from Session 27, and connect it to a CountryExplorer built entirely through the repository's interface.

What you will build

A file called repository_lab.py.

Step-by-step instructions

1

Create the file with Country and the mock import

# repository_lab.py
from mock_countries import MOCK_COUNTRIES

class Country:
    def __init__(self, name, region, population):
        self.name = name
        self.region = region
        self.population = population

    def summary(self):
        return f"{self.name} ({self.region}): pop. {self.population:,}"

    @classmethod
    def from_dict(cls, data):
        return cls(**data)
2

Build CountryRepository with get_all() and find_by_region()

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]

    def find_by_region(self, region):
        return [Country.from_dict(r) for r in self._raw_data if r["region"] == region]

repo = CountryRepository(raw_data=MOCK_COUNTRIES)
print(len(repo.get_all()))
print([c.name for c in repo.find_by_region("Africa")])
3

Build CountryExplorer and construct it FROM the repository

class CountryExplorer:
    def __init__(self, countries):
        self.countries = countries

    @property
    def total_population(self):
        return sum(c.population for c in self.countries)

explorer = CountryExplorer(countries=repo.get_all())
print(explorer.total_population)
4

Prove the repository can be swapped without touching CountryExplorer

Build a second, smaller repository from a hand-typed list and construct a second explorer from it โ€” CountryExplorer's code never changes.

tiny_data = [{"name": "Fiji", "region": "Oceania", "population": 900000}]
tiny_repo = CountryRepository(raw_data=tiny_data)
tiny_explorer = CountryExplorer(countries=tiny_repo.get_all())
print(tiny_explorer.total_population)  # 900000 โ€” same CountryExplorer class, different source
5

Write a comment identifying which methods belong to the repository vs the explorer, and why


5. Expected Files Changed

FileActionWhy
repository_lab.py Created A CountryRepository wrapping mock data, connected to a CountryExplorer.
docs/sessions/session-28/index.html Created This session document.
If you find yourself editing any other file, stop. This session touches exactly 2 files.

6. Commit Checkpoint

Once the lab is complete and you can explain every line, make this exact commit:

git add repository_lab.py docs/sessions/session-28/index.html
git commit -m "session-28: build a CountryRepository data-access layer around the mock data"
Do not commit until you can answer out loud: "Why did CountryExplorer not need any changes when I swapped in a completely different repository?"

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

A CountryRepository class wraps MOCK_COUNTRIES and exposes get_all() and find_by_region(region). Why not just let every part of the application import MOCK_COUNTRIES directly?

This is separation of concerns: the repository owns the detail of HOW data is fetched (a mock list today, a JSON file or real API tomorrow); everything else only needs to know WHAT methods the repository offers, not how they work internally.
Question 2 of 5

If CountryRepository.get_all() currently reads from MOCK_COUNTRIES, and later you change it to read from a JSON file instead, what code outside the repository needs to change?

This is the entire point of the abstraction: as long as get_all() keeps returning the same shape of data, its INTERNAL implementation can change freely (mock list, JSON file, real API) without touching a single line of code anywhere else.
Question 3 of 5

What is the difference between CountryRepository (this session) and CountryExplorer (Session 16)?

These are two different responsibilities: the repository knows how to get data; the explorer knows how to work with a set of already-fetched Country instances. Keeping them separate (rather than one giant class doing both) is exactly the kind of focused-class discipline from Session 16.
Question 4 of 5

Why does this separation make future testing (Layer 5) easier?

Because the repository is a clean, swappable interface, a test can construct one around exactly the tiny, controlled dataset it needs for that specific test โ€” this is precisely what Session 35 (testing the data layer) will do.
Question 5 of 5

Which method belongs on CountryRepository rather than CountryExplorer, given the separation of concerns described in this session?

get_all() is about FETCHING data from its source โ€” the repository's job. The others are about operating on an already-fetched, in-memory working set โ€” the explorer's job, as established in Session 16 and 22.

9. Reflection Questions

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

  1. Why does CountryExplorer never import mock_countries directly? What would be lost if it did?
  2. What would you need to change in CountryRepository (and ONLY in CountryRepository) to eventually read from a real file on disk instead of an in-memory list?
  3. How does this repository pattern relate to the "contract" concept introduced in Session 27?
  4. Can you think of a method that seems ambiguous โ€” could reasonably belong to either the repository or the explorer? How would you decide?

10. What Breaks If This Knowledge Is Missing?

  • Data-source lock-in: Without this separation, every part of the application that needs country data would import MOCK_COUNTRIES directly โ€” meaning switching to a real API later would require hunting down and rewriting every single one of those import sites instead of changing one class.
  • Working with real files (Session 29): The next session teaches reading actual JSON files from disk โ€” that new data source slots directly into CountryRepository's constructor, exactly because of the separation built this session.
  • Testing in isolation (Layer 5): Session 35 tests the data layer by building a CountryRepository around a small, controlled fake dataset โ€” only possible because the repository's constructor accepts any data source, a direct consequence of this session's design.

11. What We Learned

Python concept mastered: A data-access layer (repository) that hides the true source of data behind a stable interface, cleanly separated from the application logic that uses it.

Unlocks: The application's data source can now change completely โ€” mock, file, real API โ€” without touching any of the code that consumes it.

Next session: Session 29 โ€” Working with JSON Files. We give the repository a genuinely different, real source to read from: an actual JSON file on disk.