Building Utility Modules
Beyond small extracted functions, some logic is genuinely cross-cutting โ useful to many different parts of the application without belonging to any single class.
Standalone logic useful to more than one part of the app.
1. Learning Objective
By the end of this session you will be able to:
- Design a utility module containing several related, standalone functions
- Explain the difference between a utility function and a method that belongs on a class
- Build a search-relevance utility used by both the repository and the explorer
- Keep a utility module free of dependencies on any one specific class, keeping it broadly reusable
- Test a utility module thoroughly, since it will be relied upon from multiple places
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 function that checks whether a search term appears in a country's name, case-insensitively, is useful both inside CountryExplorer and potentially inside CountryRepository. Where should it live?
What is the key difference between a utility function like matches_search_term(name, term) and an instance method like Country.summary(self)?
Why should a utility module avoid importing or depending on a specific class like Country, if it can be avoided?
matches_search_term(name: str, term: str) is more broadly reusable and simpler to test than one that requires a full Country instance just to check a string match โ genericity is a design choice that increases reusability.Why does a utility module deserve especially thorough test coverage, more so perhaps than a one-off helper used in only one place?
Where does this new search.py utility module fit into the package structure established in Session 36?
3. The Concept โ Utility Modules
A method needs an instance (self) to run. A utility function only needs its explicit arguments โ usable anywhere, by anything.
Logic that belongs to no single class
Some logic is genuinely cross-cutting โ useful in more than one place, but not naturally "owned" by any one class's data. This is exactly what a utility module is for.
# country_explorer/search.py
def matches_search_term(name, term):
"""Check whether a search term appears in a name, case-insensitively.
Args:
name: the string to search within.
term: the search term.
Returns:
True if term appears anywhere in name, ignoring case.
"""
return term.lower() in name.lower()
Method vs standalone utility function
A method (Session 14) is bound to a specific instance's data via self. A utility function takes everything it needs as plain, explicit arguments, with no ties to any one class โ this makes it broadly reusable.
# Method โ bound to a specific Country instance
class Country:
def name_matches(self, term):
return term.lower() in self.name.lower()
# Utility function โ takes explicit arguments, usable anywhere a string is available
def matches_search_term(name, term):
return term.lower() in name.lower()
# The utility version works even without a Country instance at all:
print(matches_search_term("Kenya", "ken")) # True โ no Country object needed
Using the utility from multiple places
Both CountryExplorer and CountryRepository can use the same search utility without either one owning it or duplicating the logic.
# models.py
from .search import matches_search_term
class CountryExplorer:
def search(self, term):
return [c for c in self.countries if matches_search_term(c.name, term)]
# repository.py
from .search import matches_search_term
class CountryRepository:
def search(self, term):
return [c for c in self.get_all() if matches_search_term(c.name, term)]
Thorough testing for widely-used utilities
Because this function will be relied upon from multiple places, it deserves especially thorough test coverage โ a bug here has a wide blast radius.
# tests/test_search.py
from country_explorer.search import matches_search_term
def test_matches_exact():
assert matches_search_term("Kenya", "Kenya") is True
def test_matches_partial():
assert matches_search_term("Kenya", "ken") is True
def test_matches_case_insensitive():
assert matches_search_term("KENYA", "kenya") is True
def test_no_match():
assert matches_search_term("Kenya", "xyz") is False
def test_empty_term_matches_everything():
assert matches_search_term("Kenya", "") is True # an empty string is "in" every string
4. Lab
What you will build
A new module country_explorer/search.py plus tests/test_search.py.
Step-by-step instructions
Create search.py with matches_search_term and a clear docstring
# country_explorer/search.py
def matches_search_term(name, term):
"""Check whether a search term appears in a name, case-insensitively.
Args:
name: the string to search within.
term: the search term.
Returns:
True if term appears anywhere in name, ignoring case.
"""
return term.lower() in name.lower()
Add a search() method to CountryExplorer using the utility
# country_explorer/models.py
from .search import matches_search_term
class CountryExplorer:
# ... existing code ...
def search(self, term):
return [c for c in self.countries if matches_search_term(c.name, term)]
Add a search() method to CountryRepository using the same utility
# country_explorer/repository.py
from .search import matches_search_term
class CountryRepository:
# ... existing code ...
def search(self, term):
return [c for c in self.get_all() if matches_search_term(c.name, term)]
Write thorough tests for matches_search_term covering multiple cases
# tests/test_search.py
from country_explorer.search import matches_search_term
def test_matches_exact():
assert matches_search_term("Kenya", "Kenya") is True
def test_matches_partial():
assert matches_search_term("Kenya", "ken") is True
def test_matches_case_insensitive():
assert matches_search_term("KENYA", "kenya") is True
def test_no_match():
assert matches_search_term("Kenya", "xyz") is False
Add tests confirming CountryExplorer.search() and CountryRepository.search() both use it correctly
# tests/test_search_integration.py
from country_explorer import Country, CountryExplorer, CountryRepository
def test_explorer_search_uses_matches_search_term():
explorer = CountryExplorer(countries=[Country(name="Kenya", region="Africa", population=1)])
assert [c.name for c in explorer.search("ken")] == ["Kenya"]
def test_repository_search_uses_matches_search_term():
repo = CountryRepository(raw_data=[{"name": "Kenya", "region": "Africa", "population": 1}])
assert [c.name for c in repo.search("ken")] == ["Kenya"]
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
country_explorer/search.py |
Created | A generic, class-independent search-matching utility. |
country_explorer/models.py |
Modified | CountryExplorer.search() uses the shared utility. |
country_explorer/repository.py |
Modified | CountryRepository.search() uses the same shared utility. |
tests/test_search.py |
Created | Thorough, direct tests for the utility function itself. |
tests/test_search_integration.py |
Created | Confirms both classes correctly use the shared utility. |
docs/sessions/session-38/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 country_explorer/search.py country_explorer/models.py country_explorer/repository.py tests/test_search.py tests/test_search_integration.py docs/sessions/session-38/index.html
git commit -m "session-38: build a shared search utility used by both CountryExplorer and CountryRepository"
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 function that checks whether a search term appears in a country's name, case-insensitively, is useful both inside CountryExplorer and potentially inside CountryRepository. Where should it live?
What is the key difference between a utility function like matches_search_term(name, term) and an instance method like Country.summary(self)?
Why should a utility module avoid importing or depending on a specific class like Country, if it can be avoided?
matches_search_term(name: str, term: str) is more broadly reusable and simpler to test than one that requires a full Country instance just to check a string match โ genericity is a design choice that increases reusability.Why does a utility module deserve especially thorough test coverage, more so perhaps than a one-off helper used in only one place?
Where does this new search.py utility module fit into the package structure established in Session 36?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Why does making matches_search_term take plain strings (rather than a Country instance) make it more broadly reusable than it would otherwise be?
- If matches_search_term had a bug, how many parts of the application would be affected? How does that compare to a bug in a method used by only one class?
- Why do you think this session tested the utility function directly AND wrote separate integration tests for each class that uses it โ rather than only testing one or the other?
- Can you think of another piece of logic in the project that might deserve its own class-independent utility module, following this same pattern?
10. What Breaks If This Knowledge Is Missing?
- Duplicated search logic drifting apart: Without a shared utility, CountryExplorer and CountryRepository would each implement their own version of "does this term match this name" โ and a future improvement (like trimming whitespace before comparing) could easily be applied to one and forgotten in the other.
- The prop drilling problem (Session 39): The next session examines what happens when data needs to be threaded through many layers just to reach where it is needed โ a related but distinct architecture problem from the code-duplication issue this session solved.
- Architecture review (Session 40): This session's search.py is a concrete example of the "focused, well-tested, broadly reusable module" pattern that Session 40's architecture review will assess the whole project against.
11. What We Learned
Python concept mastered: Designing and thoroughly testing a standalone, class-independent utility module, and understanding when logic belongs to a class versus a shared utility.
Unlocks: The application now has a genuinely shared, well-tested search capability used consistently across multiple parts of the codebase, with zero duplication.
Next session: Session 39 โ The Prop Drilling Problem. We deliberately design ourselves into an architecture problem โ data that needs to be threaded through several layers just to reach where it is actually used.