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.
A single, swappable gateway between the app and its data.
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.
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?
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?
What is the difference between CountryRepository (this session) and CountryExplorer (Session 16)?
Why does this separation make future testing (Layer 5) easier?
Which method belongs on CountryRepository rather than CountryExplorer, given the separation of concerns described in this session?
3. The Concept โ A Data Access Layer
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
What you will build
A file called repository_lab.py.
Step-by-step instructions
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)
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")])
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)
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
Write a comment identifying which methods belong to the repository vs the explorer, and why
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
repository_lab.py |
Created | A CountryRepository wrapping mock data, connected to a CountryExplorer. |
docs/sessions/session-28/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 repository_lab.py docs/sessions/session-28/index.html
git commit -m "session-28: build a CountryRepository data-access layer around the mock 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.
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?
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?
What is the difference between CountryRepository (this session) and CountryExplorer (Session 16)?
Why does this separation make future testing (Layer 5) easier?
Which method belongs on CountryRepository rather than CountryExplorer, given the separation of concerns described in this session?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does CountryExplorer never import mock_countries directly? What would be lost if it did?
- 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?
- How does this repository pattern relate to the "contract" concept introduced in Session 27?
- 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.