Unit 11 · lesson

Building Your Own Modules

Core path: 30 minutes

When does splitting a project into multiple files make the program easier to understand instead of harder to navigate?

Multiple files are not automatically architecture.

A project with:

thing1.py
thing2.py
helpers2.py
misc.py

is technically modular in the sense that it has several modules. It may still be a mess.

The useful goal is to separate responsibilities behind names and boundaries that another developer can follow.

Start from the monolith you already understand

Imagine the roster project has grown into one main.py containing:

load JSON
save JSON
create players
calculate ranks
search players
show menus
print reports
coordinate user choices

The problem is not a magical 600-line threshold.

The problem is that one file now owns several different reasons to change.

If storage changes from JSON to something else, why should the menu code be mixed into the same area?

If report formatting changes, why should you have to scroll through file-loading logic?

Those are architecture questions.

Separate by responsibility

A reasonable small design might be:

player-roster/
├── main.py
├── storage.py
├── players.py
└── display.py

Possible responsibilities:

storage.py
load/save persistent data

players.py
player-specific creation, rank rules, search logic

display.py
console presentation and menus

main.py
coordinate the workflow

There are other valid arrangements.

The standard is not "four files exactly." The standard is whether the boundaries make the system easier to reason about and test.

Build one import boundary by hand

Create:

# tools.py

def double(number):
    return number * 2

Then:

# main.py
import tools

result = tools.double(5)
print(result)

Execution now crosses a module boundary:

main.py
  ↓ imports tools
main can resolve tools.double
  ↓ call
function defined in tools.py executes
  ↓ return value
main.py receives result

The function code did not get copied into main.py as text.

Python loaded/imported the module and made its namespace available.

main.py is often a coordinator, not a dumping ground

A readable coordinator can look like:

import storage
import players
import display

player_list = storage.load_players()
display.show_menu()
new_player = players.create_player()
player_list.append(new_player)
storage.save_players(player_list)

You can read the high-level workflow without opening every implementation immediately.

That is one benefit of abstraction at the module level.

Do not overapply the rule and make main.py artificially empty while hiding one giant helpers.py elsewhere. Responsibility still matters.

Module names are part of the architecture

Compare:

functions.py
utils.py
stuff.py

with:

storage.py
reports.py
validation.py

The second group tells the reader what kind of responsibility to expect.

A vague utils.py can become a junk drawer where unrelated code goes because nobody wants to decide where it belongs.

Sometimes a small utilities module is legitimate. Do not let the label replace design thinking.

Imports create dependency direction

Suppose:

main.py imports storage.py
storage.py imports json

The dependency direction is visible:

main

storage

json

Now imagine storage.py imports main.py while main.py already imports storage.py.

You can create circular-import problems where modules depend on each other during initialization.

A clean architecture often tries to keep dependency direction understandable rather than creating a web where every file imports every other file.

You do not need to master circular imports this week. You should recognize that module relationships are architecture, not just file organization.

Importing a module executes its top-level code

This surprises beginners.

Suppose tools.py contains:

print("TOOLS MODULE LOADED")


def double(number):
    return number * 2

Then main.py does:

import tools

The top-level print() in tools.py can run when the module is imported.

That is why reusable modules generally avoid unrelated interactive behavior at import time.

A file meant to provide functions should not unexpectedly launch menus or ask for user input merely because another module imported it.

Use the __name__ guard to separate library behavior from direct execution

A common pattern:

def main():
    print("Run application")


if __name__ == "__main__":
    main()

When the file is executed directly, its __name__ is typically "__main__", so the application entry function runs.

When the file is imported as a module, that guarded block does not run in the same way.

You do not need to use this in every tiny project. Understand what problem it solves: importing reusable code should not accidentally execute the whole application.

Naming collisions can hijack imports

Do not casually create:

random.py
json.py
csv.py

inside the project when you also expect to import those standard-library modules.

Your local file can be resolved instead of the library module you intended.

If an import behaves strangely, inspect:

import random
print(random.__file__)

That path is evidence about which module actually loaded.

Guided refactor

Start with:

# main.py
import json


def load_players():
    ...


def save_players(players):
    ...


def calculate_rank(score):
    ...


def show_menu():
    ...

# application loop follows

Before moving code, label each responsibility.

Then choose one boundary first. For example, move persistence functions into storage.py.

Update imports and run the application.

Only after that boundary works should you continue with the next module.

That turns a large refactor into controlled slices you can verify.

Before Lesson 3

You should be able to explain:

  • why several files do not automatically create good architecture;
  • what responsibility each module in your project owns;
  • what import tools gives the caller;
  • why top-level code inside an imported module can matter;
  • what the if __name__ == "__main__" guard is protecting;
  • how a poor filename can interfere with imports.

Lesson 3 adds code that lives outside your project entirely: third-party packages and their environments.

Vocabulary lab

Flip the idea, not just the card

Explain the term before you reveal the back. Then compare your explanation with the definition, example, and warning.

1 / 5
Read all terms without animation
Architecture
The organization, responsibilities, and relationships of the major pieces of a software system. Example: Separating storage, presentation, and application coordination into deliberate modules. Do not confuse it with: Simply having many source files.
Module Boundary
A separation where code in one module is accessed by another through imports and public names. Example: main.py calling storage.load_players(). Do not confuse it with: Copying the same function implementation into both files.
Dependency Direction
The direction in which one module relies on code from another module or package. Example: main.py depends on storage.py because main imports storage. Do not confuse it with: The order files appear in the Explorer.
Top-Level Code
Statements in a module that are not inside a function/class block and can execute while the module is loaded. Example: print('loaded') directly in tools.py. Do not confuse it with: A function body that waits until the function is called.
__main__ Guard
The if __name__ == '__main__' pattern used to run application entry behavior only when a module is executed directly. Example: Calling main() only under the guard. Do not confuse it with: An import statement that makes the entire imported application run intentionally.