Reusable Functions and Modules
With a clean package structure in place, we identify genuinely reusable logic scattered across the project and extract it into shared, well-tested functions.
Duplicated logic, pulled into one shared, tested place.
1. Learning Objective
By the end of this session you will be able to:
- Identify duplicated or near-duplicated logic across different parts of the project
- Extract that logic into a single, well-named, reusable function
- Write a docstring that documents a function's parameters, return value, and behavior clearly
- Add tests for the newly extracted function to close a coverage gap
- Distinguish "reusable" from "premature abstraction" โ extracting too early or unnecessarily
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.
Both summary() (formatting a country) and a hypothetical future feature might need to format a large number with comma separators, like 54,000,000. If this formatting appears in two places with near-identical code, what should you do?
What is the purpose of a docstring like """Format a population count with comma separators.\n\nArgs:\n value: the population as an int\nReturns:\n A string like '54,000,000'\n""" right under a function definition?
help(function_name) and by anyone reading the source โ especially valuable for a function meant to be reused by other parts of the project.Why should a newly extracted reusable function get its own tests, even if the code it was extracted FROM was already tested indirectly?
What is "premature abstraction," and why is it a real risk when looking for things to extract?
Where is the most sensible place, given Session 36's new package structure, to put a genuinely reusable formatting function used across multiple parts of the project?
3. The Concept โ Extracting Reusable Logic
Two near-identical formatting expressions collapse into one shared, tested, documented function.
Spotting duplicated logic
As a project grows, similar formatting or calculation logic tends to appear in more than one place. The first step is noticing it โ comparing what several pieces of code are actually doing, not just how they look on the surface.
# Duplication scattered across the project:
# models.py
def summary(self):
return f"{self.name} ({self.region}): pop. {self.population:,}"
# some future report-generation code, formatting the same kind of number again
def population_report_line(name, population):
return f"{name}: {population:,} people"
Extracting one shared function
Once genuine, ongoing duplication is identified, extract it into one well-named function, and update every call site to use it โ exactly the "one place to change" principle from Session 10.
# country_explorer/formatting.py
def format_population(value):
"""Format a population count with comma separators.
Args:
value: the population as an int.
Returns:
A string like "54,000,000".
"""
return f"{value:,}"
# models.py
from .formatting import format_population
def summary(self):
return f"{self.name} ({self.region}): pop. {format_population(self.population)}"
Documenting the extracted function with a docstring
A function meant to be reused across the project deserves clear documentation of what it takes and what it returns, directly alongside the code โ readable via help() and by anyone reading the source.
Testing the extracted function directly
A focused test of the standalone function is simpler than relying on it being indirectly exercised through summary() or other larger tests.
# tests/test_formatting.py
from country_explorer.formatting import format_population
def test_format_population_adds_commas():
assert format_population(54000000) == "54,000,000"
def test_format_population_small_number():
assert format_population(5) == "5"
When NOT to extract โ premature abstraction
If two pieces of code happen to look similar right now, but represent genuinely different concepts likely to change independently, forcing them into a shared function adds coupling without a real benefit. Extraction should follow real, ongoing duplication of one actual idea โ not surface-level resemblance.
4. Lab
What you will build
A new module country_explorer/formatting.py plus tests/test_formatting.py.
Step-by-step instructions
Create formatting.py with format_population and a clear docstring
# country_explorer/formatting.py
def format_population(value):
"""Format a population count with comma separators.
Args:
value: the population as an int.
Returns:
A string like "54,000,000".
"""
return f"{value:,}"
Update models.py's summary() to use the extracted function
# country_explorer/models.py
from .formatting import format_population
class Country:
# ... existing code ...
def summary(self):
return f"{self.name} ({self.region}): pop. {format_population(self.population)}"
Write direct tests for format_population
# tests/test_formatting.py
from country_explorer.formatting import format_population
def test_format_population_adds_commas():
assert format_population(54000000) == "54,000,000"
def test_format_population_small_number():
assert format_population(0) == "0"
Run the full test suite, confirming summary()'s existing tests still pass unchanged
This proves the extraction was behavior-preserving.
# pytest -v
Write a short comment describing a case of near-identical-looking code you deliberately did NOT merge, and why
This exercises the "premature abstraction" judgment call โ invent a plausible example if needed, e.g. two functions that both happen to check "value > 0" but for conceptually different reasons.
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
country_explorer/formatting.py |
Created | A shared, documented, reusable population formatting function. |
country_explorer/models.py |
Modified | summary() now uses the extracted, shared function. |
tests/test_formatting.py |
Created | Direct, focused tests for the extracted function. |
docs/sessions/session-37/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/formatting.py country_explorer/models.py tests/test_formatting.py docs/sessions/session-37/index.html
git commit -m "session-37: extract and test a reusable format_population function"
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.
Both summary() (formatting a country) and a hypothetical future feature might need to format a large number with comma separators, like 54,000,000. If this formatting appears in two places with near-identical code, what should you do?
What is the purpose of a docstring like """Format a population count with comma separators.\n\nArgs:\n value: the population as an int\nReturns:\n A string like '54,000,000'\n""" right under a function definition?
help(function_name) and by anyone reading the source โ especially valuable for a function meant to be reused by other parts of the project.Why should a newly extracted reusable function get its own tests, even if the code it was extracted FROM was already tested indirectly?
What is "premature abstraction," and why is it a real risk when looking for things to extract?
Where is the most sensible place, given Session 36's new package structure, to put a genuinely reusable formatting function used across multiple parts of the project?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Before this session, summary()'s formatting logic was only tested indirectly through summary()'s own tests. Why is a direct test of format_population still valuable now that it exists?
- Can you find another piece of logic anywhere in the project (Sessions 1-32) that is duplicated and could benefit from this same extraction treatment?
- What is the cost of extracting something too early, before a second real use case actually exists? Contrast that with the cost of NOT extracting genuinely duplicated logic.
- How does docstring documentation differ in purpose from the reflection comments you've written throughout this curriculum?
10. What Breaks If This Knowledge Is Missing?
- Silent formatting drift: Without extracting shared formatting logic, a future change to how numbers should display (e.g. adding a currency symbol) risks being applied in one place and forgotten in another, producing visibly inconsistent output across the application.
- Utility modules (Session 38): The next session builds on this exact pattern, extracting more substantial, cross-cutting utility logic into its own dedicated module โ formatting.py is the first, smallest example of that broader pattern.
- Recognizing coupling problems (Session 39): Learning to correctly judge WHEN to extract (and when NOT to, per premature abstraction) is a prerequisite for Session 39's harder judgment call: recognizing when a class has taken on too much responsibility.
11. What We Learned
Python concept mastered: Identifying genuine duplication, extracting it into a documented, tested, reusable function, and recognizing premature abstraction as a real risk to avoid.
Unlocks: You can now confidently spot and safely extract reusable logic from a growing codebase, backed by tests that prove the extraction changed nothing about behavior.
Next session: Session 38 โ Building Utility Modules. We build a more substantial, standalone utility module โ logic that supports the whole application without being tied to any one class.