Layer 6 Session 36 Architecture

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.

pkg/ models.py repository.py

One growing file, split into focused, named modules.

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:

  • 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.

Question 1 of 5

What is the difference between a module (Session 10) and a package?

A module (Session 10) is one file. A package is a directory of related modules, grouped together and marked as an importable unit by an __init__.py file (which can be empty or can re-export names for convenience).
Question 2 of 5

What is the minimum required for a directory to be treated as a regular Python package?

An __init__.py file (even completely empty) is what traditionally marks a directory as a Python package, making from mypackage import something work.
Question 3 of 5

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?

Moving a class to a new module means every import referencing its old location must be updated to the new path โ€” this is a real, visible cost of reorganizing, which is exactly why a test suite (Layer 5) is valuable for confirming nothing was missed.
Question 4 of 5

Why should you run the full test suite immediately after reorganizing files into a package, before making any other changes?

This is Layer 5's entire payoff: a reorganization SHOULD change nothing about behavior, only structure. Running the test suite immediately after confirms that promise held โ€” if any test fails, an import was missed or something subtly broke.
Question 5 of 5

What is a reasonable way to split country.py's growing content across a package, based on Session 28's separation of concerns?

Good package organization follows the conceptual boundaries already established by the project's design (Session 28's repository vs explorer distinction, Session 24's separate validators module) โ€” grouping by responsibility, not arbitrarily.

3. The Concept โ€” Modules vs Packages

COUNTRY.PYone growingfileCOUNTRY_EXPLORER/models.pyrepository.pyvalidators.py

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

Lab objective: Reorganize country.py into a country_explorer/ package with focused submodules, update all imports, and confirm the full test suite still passes.

What you will build

A new package directory country_explorer/ replacing the flat country.py.

Step-by-step instructions

1

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
2

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)
3

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]
4

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
5

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

FileActionWhy
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.
If you find yourself editing any other file, stop. This session touches exactly 7 files.

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"
Do not commit until you can answer out loud: "Why did running the full test suite right after this reorganization matter more than after almost any previous session?"

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

What is the difference between a module (Session 10) and a package?

A module (Session 10) is one file. A package is a directory of related modules, grouped together and marked as an importable unit by an __init__.py file (which can be empty or can re-export names for convenience).
Question 2 of 5

What is the minimum required for a directory to be treated as a regular Python package?

An __init__.py file (even completely empty) is what traditionally marks a directory as a Python package, making from mypackage import something work.
Question 3 of 5

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?

Moving a class to a new module means every import referencing its old location must be updated to the new path โ€” this is a real, visible cost of reorganizing, which is exactly why a test suite (Layer 5) is valuable for confirming nothing was missed.
Question 4 of 5

Why should you run the full test suite immediately after reorganizing files into a package, before making any other changes?

This is Layer 5's entire payoff: a reorganization SHOULD change nothing about behavior, only structure. Running the test suite immediately after confirms that promise held โ€” if any test fails, an import was missed or something subtly broke.
Question 5 of 5

What is a reasonable way to split country.py's growing content across a package, based on Session 28's separation of concerns?

Good package organization follows the conceptual boundaries already established by the project's design (Session 28's repository vs explorer distinction, Session 24's separate validators module) โ€” grouping by responsibility, not arbitrarily.

9. Reflection Questions

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

  1. 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?
  2. 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?
  3. 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?
  4. 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.