Package and Folder Organization
Layer 6 begins. Our project has grown past what flat files can comfortably hold. We reorganize into a proper Python package structure.
One growing file, split into focused, named modules.
1. Learning Objective
By the end of this session you will be able to:
- Explain the difference between a module (Session 10) and a package
- Create a package using an __init__.py file
- Reorganize country.py's growing classes into a package with focused submodules
- Update import statements across the project to match the new structure
- Run the existing test suite after reorganizing, to confirm nothing broke
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.
What is the difference between a module (Session 10) and a package?
__init__.py file (which can be empty or can re-export names for convenience).What is the minimum required for a directory to be treated as a regular Python package?
__init__.py file (even completely empty) is what traditionally marks a directory as a Python package, making from mypackage import something work.You split country.py into country_explorer/models.py (Country, CountryExplorer), country_explorer/repository.py (CountryRepository), and country_explorer/validators.py. What must change in files that used to do from country import Country?
Why should you run the full test suite immediately after reorganizing files into a package, before making any other changes?
What is a reasonable way to split country.py's growing content across a package, based on Session 28's separation of concerns?
3. The Concept โ Modules vs Packages
country.py splits into a package: focused submodules, with __init__.py re-exporting the names external code actually needs.
From one growing file to a package
country.py has grown to contain Country, CountryExplorer, CountryRepository, and validate_country_record โ several distinct responsibilities crammed into one file. A package lets us split these while keeping them organized under one importable unit.
# Before โ everything in one growing file
# country.py
class Country: ...
class CountryExplorer: ...
class CountryRepository: ...
def validate_country_record(data): ...
Creating a package with __init__.py
A directory becomes a package once it contains an __init__.py file. The file can be empty, or it can re-export names to make imports more convenient for users of the package.
# country_explorer/__init__.py
from .models import Country, CountryExplorer
from .repository import CountryRepository
from .validators import validate_country_record
# This lets other code do: from country_explorer import Country
# instead of the more verbose: from country_explorer.models import Country
Splitting by responsibility
Following the conceptual boundaries already established in the project โ Session 28's repository/explorer split, Session 24's separate validators โ gives a natural, sensible package layout.
# country_explorer/models.py
class Country:
...
class CountryExplorer:
...
# country_explorer/repository.py
from .models import Country
class CountryRepository:
...
# country_explorer/validators.py
def validate_country_record(data):
...
Updating imports and re-running the test suite
Every file that imported from country now needs to import from country_explorer (or a specific submodule). Running the full test suite immediately afterward confirms the reorganization did not silently break anything โ the payoff of Layer 5's investment.
# tests/test_country.py โ before
# from country import Country
# tests/test_country.py โ after
from country_explorer import Country
# Then: pytest -v
# If every test still passes, the reorganization was behavior-preserving, as intended
4. Lab
What you will build
A new package directory country_explorer/ replacing the flat country.py.
Step-by-step instructions
Create the country_explorer/ directory with __init__.py
# country_explorer/__init__.py
from .models import Country, CountryExplorer
from .repository import CountryRepository
from .validators import validate_country_record
Move Country and CountryExplorer into models.py
# country_explorer/models.py
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:,}"
def set_population(self, value):
if value < 0:
raise ValueError(f"population must be non-negative, got {value}")
self.population = value
def grow_population(self, amount):
if amount < 0:
raise ValueError(f"amount must be non-negative, got {amount}")
self.population += amount
@classmethod
def from_dict(cls, data):
return cls(**data)
class CountryExplorer:
def __init__(self, countries):
self.countries = countries
def add_country(self, country):
if not isinstance(country, Country):
raise TypeError(f"expected a Country instance, got {type(country).__name__}")
self.countries.append(country)
@property
def country_count(self):
return len(self.countries)
@property
def total_population(self):
return sum(c.population for c in self.countries)
Move CountryRepository into repository.py, importing Country from models
# country_explorer/repository.py
from .models import Country
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]
Move validate_country_record into validators.py
# country_explorer/validators.py
def validate_country_record(data):
required = {"name": str, "region": str, "population": int}
for key, expected_type in required.items():
if key not in data:
raise ValueError(f"missing required field: {key}")
if not isinstance(data[key], expected_type):
raise TypeError(f"{key} must be {expected_type.__name__}, got {type(data[key]).__name__}")
return data
Update every test file's imports and run the full suite
Change every from country import ... to from country_explorer import ..., delete the old country.py, and run pytest -v.
# tests/test_country.py, test_return_values.py, test_state_changes.py, test_repository.py
# change: from country import Country, CountryExplorer, CountryRepository, validate_country_record
# to: from country_explorer import Country, CountryExplorer, CountryRepository, validate_country_record
# pytest -v
5. Expected Files Changed
| File | Action | Why |
|---|---|---|
country_explorer/__init__.py |
Created | Marks the directory as a package and re-exports the key names. |
country_explorer/models.py |
Created | Country and CountryExplorer, moved from country.py. |
country_explorer/repository.py |
Created | CountryRepository, moved from country.py. |
country_explorer/validators.py |
Created | validate_country_record, moved from country.py. |
country.py |
Deleted | Replaced by the new country_explorer package. |
tests/*.py |
Modified | Import paths updated to the new package structure. |
docs/sessions/session-36/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/ tests/ docs/sessions/session-36/index.html
git rm country.py
git commit -m "session-36: reorganize into a country_explorer package with focused submodules"
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.
What is the difference between a module (Session 10) and a package?
__init__.py file (which can be empty or can re-export names for convenience).What is the minimum required for a directory to be treated as a regular Python package?
__init__.py file (even completely empty) is what traditionally marks a directory as a Python package, making from mypackage import something work.You split country.py into country_explorer/models.py (Country, CountryExplorer), country_explorer/repository.py (CountryRepository), and country_explorer/validators.py. What must change in files that used to do from country import Country?
Why should you run the full test suite immediately after reorganizing files into a package, before making any other changes?
What is a reasonable way to split country.py's growing content across a package, based on Session 28's separation of concerns?
9. Reflection Questions
Think through these after the post-quiz. No right answer โ they are for discussion.
- Did any test fail immediately after the reorganization? If so, what did that reveal about a missed import? If not, what does that tell you about how thorough Layer 5's coverage really was?
- Why does __init__.py re-export names instead of requiring every caller to know the exact submodule (models, repository, validators) a given class lives in?
- How does this package split reflect the conceptual boundaries you've been building since Session 28 (repository) and Session 24 (validators), rather than an arbitrary split?
- What would you do differently if you needed to add a FOURTH responsibility to this package later โ how would you decide whether it deserves its own submodule?
10. What Breaks If This Knowledge Is Missing?
- Import errors from a rushed reorganization: Reorganizing files without updating every import reference (and without a test suite to catch the ones you miss) is one of the most common sources of "it worked yesterday" bugs in real projects.
- Reusable modules (Session 37): The next session builds genuinely reusable utility functions โ having a clean package structure in place first makes it obvious where new shared code should live.
- God objects and tight coupling (Session 39): A clean package split makes it much easier to SEE when one module starts doing too much or reaching too deeply into another's internals โ the problem Session 39 addresses directly.
11. What We Learned
Python concept mastered: Packages vs modules, creating a package with __init__.py, and safely reorganizing a growing codebase with a test suite as a safety net.
Unlocks: The project now has a real, scalable package structure instead of one growing flat file โ and you have proven, with tests, that the reorganization changed nothing about behavior.
Next session: Session 37 โ Reusable Functions and Modules. We extract genuinely reusable functions and utility modules, building on the clean structure we just established.