C. Designing Modular Programs · Modular and Object-Oriented DesignModules and Packages: Programs Across Multiple Files
A2.1 C1.4About 70 minutes
Learning goals- I can split a program into several module files and import from them in different ways.
- I can explain what a namespace is and why
module.name prevents name clashes. - I can use
if __name__ == "__main__": so a module can be both imported and run as a self-test. - I can organize modules into a package with an
__init__.py file. - I can predict where Python looks for a module and avoid shadowing a standard-library module.
Words to know
module
A single Python file (.py) whose functions, classes and variables can be imported by other files.
package
A folder of modules, marked by an __init__.py file, imported with dotted names such as carnival.timing.
library
A collection of reusable modules or packages, such as Python's standard library (math, random, json, datetime).
namespace
A separate set of names. Each module has its own, so two modules can both define a function called load without clashing.
import
A statement that runs a module (the first time) and makes its names available in the current file.
alias
A shorter or different name given with as, for example import statistics as stats.
shadowing
When a file of your own has the same name as a library module, so Python imports yours instead.
public interface (API)
The functions and classes a module intends others to use. By convention, names starting with an underscore are internal.
From one file to many
The race report from the last lesson has helpers, such as format_time(), that the ski club, the swim team and the track team could all use. If each team copies the function into its own program, a bug fix must be made in every copy. The fix: put the shared code in its own file and let every program import it.
Curriculum expectation A2.1 asks you to create a modular program divided among multiple files. Every serious program is built this way. The Python standard library itself is hundreds of modules, and a typical app imports dozens of them.
Remember from Grade 11
In ICS3U you saved functions in geometry.py and wrote import geometry in another file in the same folder, then called geometry.circle_area(15). You also used import math and import random. Today you learn the other forms of import, how Python finds modules, how a module can test itself, and how to group modules into packages.
Four ways to import
import timing brings in one name, the module. You call timing.format_time(65). This is the clearest form, because every call shows where the function comes from.from timing import format_time copies one name into your file, so you can call format_time(65) directly. Handy, but a reader must scroll up to see where it came from.import statistics as stats gives the module an alias, a shorter name.from timing import * copies every public name. Avoid it in real programs: it can silently overwrite names you already have, and nobody can tell where a name came from.
The first time a module is imported, Python runs the whole file from top to bottom, creating its functions and variables, and keeps the resulting module object. Importing it again elsewhere reuses that object; the file does not run a second time.
Each module has its own namespace (its own set of names). import timing adds just one name, timing, to report.py; the functions stay inside the module object and are reached with a dot.A module that can test itself
Every module has a built-in variable __name__. When you run a file directly, Python sets its __name__ to "__main__". When the file is imported, __name__ is the module's own name, such as "timing". So the test
if __name__ == "__main__":
is true only when the file is run directly. Put quick self-tests or a demo under it. They run when you test the module by itself, and they stay quiet when another program imports it.
Worked exampleExample 1: A reusable timing module
Move the race helpers into timing.py with a self-test, then use them from a second file, report.py, in the same folder.
- Create timing.py containing only definitions plus a guarded self-test. Each function gets a docstring, because other programmers will read it with
help(timing). - Run timing.py by itself. Its
__name__ is "__main__", so the asserts run and it prints timing.py self-test passed. - Create report.py with
import timing at the top. Imports always go at the top of a file, so a reader sees the dependencies first. - When report.py imports timing, timing's
__name__ is "timing", so the self-test does not run. report.py calls timing.parse_line(...) and timing.format_time(...). - The last line of report.py prints both names: report.py itself is
__main__ (it was run directly) and the module is timing.
Running timing.py prints timing.py self-test passed. Running report.py prints the two lines shown below its code.
"""Timing helpers for race results."""
def format_time(seconds):
"""Turn whole seconds into "m:ss" text."""
return f"{seconds // 60}:{seconds % 60:02d}"
def parse_line(line):
"""Split "name,seconds" into a (name, seconds) tuple."""
name, secs = line.split(",")
return name, int(secs)
if __name__ == "__main__":
# Self-test: runs only when this file is run directly.
assert format_time(65) == "1:05"
assert parse_line("Ben,298") == ("Ben", 298)
print("timing.py self-test passed")
timing.py, run directly. Output: timing.py self-test passed
import timing
name, secs = timing.parse_line("Chloé,305")
print(name, timing.format_time(secs))
print(__name__, timing.__name__)
report.py, in the same folder as timing.py.
Chloé 5:05
__main__ timing
Output of the program above (what Python prints).
Packages: folders of modules
When a project grows past a handful of modules, group related modules into a package: a folder that contains a file named __init__.py. The __init__.py file runs when the package is first imported. It can be empty, hold a docstring describing the package, or import the names the package wants to offer.
You reach modules inside a package with dotted names: import carnival.timing, from carnival import scoring, or from carnival.timing import format_time. Large libraries nest packages inside packages the same way, for example xml.etree.ElementTree in the standard library.
The layout for Example 2. carnival is a package (a folder containing __init__.py), and each .py file inside it is a module. app.py imports from the package with dotted names such as carnival.timing.Worked exampleExample 2: A carnival package
Build the package shown in the folder diagram. scoring.py must reuse format_time from timing.py, and app.py must use both modules.
- carnival/__init__.py holds just a docstring. Its presence makes
carnival a package. - carnival/timing.py defines
format_time. It does not need to know who will use it. - carnival/scoring.py imports
format_time with an absolute dotted import, from carnival.timing import format_time. One module of a package reusing another is normal and good: the formatting rule lives in exactly one place. - app.py sits next to the carnival folder (not inside it) and is run from there. It imports the scoring module, and imports format_time under the alias
ft. points_for(7) returns 0 because the POINTS list only covers places 1 to 5; the if checks the bounds before indexing.
The three package files and app.py are below. Running app.py prints 1. Ben 4:58 (10 pts), 0 and 5:30.
"""carnival: tools for scoring winter carnival events."""
carnival/__init__.py
"""Time formatting for carnival events."""
def format_time(seconds):
"""Turn whole seconds into "m:ss" text."""
return f"{seconds // 60}:{seconds % 60:02d}"
carnival/timing.py
"""Scoring rules for carnival events."""
from carnival.timing import format_time
POINTS = [10, 8, 6, 5, 4]
def points_for(place):
"""Return the points for a finishing place (1 = first); 0 after 5th."""
if 1 <= place <= len(POINTS):
return POINTS[place - 1]
return 0
def score_line(place, name, seconds):
"""Return one formatted line of the results table."""
return f"{place}. {name} {format_time(seconds)} ({points_for(place)} pts)"
carnival/scoring.py
from carnival import scoring
from carnival.timing import format_time as ft
print(scoring.score_line(1, "Ben", 298))
print(scoring.points_for(7))
print(ft(330))
app.py, in the folder that contains the carnival folder.
1. Ben 4:58 (10 pts)
0
5:30
Output of the program above (what Python prints).
Where does Python look?
When you write import timing, Python searches a list of folders called the module search path (you can print it with import sys then print(sys.path)). In order, it looks in:
- the folder of the script you ran,
- folders named in the PYTHONPATH setting, if any,
- the standard library,
- the folder of installed third-party packages (site-packages).
(A few modules, such as sys, are built into Python itself and are found before this list is searched; on some installations (Windows, for example) math is one of them, so a file named math.py may not shadow it.)
The first match wins. That gives a classic bug: if you save your own file as random.py or statistics.py, then import random in that folder finds your file and not the library. This is called shadowing.
import random
print(hasattr(random, "randint"))
main.py, saved in a folder that also contains a student's file called random.py with the single line print("my random.py was imported!"). The library's randint is missing because Python imported the wrong file.
my random.py was imported!
False
Output of the program above (what Python prints).
Designing a module's public interface
- Give each module one theme (time formatting, scoring, file loading). That is cohesion at the file level.
- Start internal helper names with an underscore, such as
_clean(text). It tells other programmers "not part of the public interface; may change". - Put a docstring at the top of every module saying what it is for.
help(timing) shows it. - Avoid circular imports (a imports b while b imports a). They usually mean two modules should be merged or a third module should hold the shared code.
- Name files in lowercase with underscores (
race_tools.py), never with spaces or a leading digit, or they cannot be imported.
Just for funPython has a joke built in: type import this in the Python shell and it prints "The Zen of Python", a short poem of design advice. Its last line is: "Namespaces are one honking great idea -- let's do more of those!"
Hands-on activityBuild a three-file unit converter
You need: a computer with Python (IDLE, Thonny or VS Code) and a new empty folder.
- Create convert.py with three documented functions:
c_to_f(c) returns 9c/5 + 32, cm_to_in(cm) returns cm / 2.54, and kmh_to_ms(kmh) returns kmh / 3.6. Add a self-test under if __name__ == "__main__": with one assert per function (for example assert c_to_f(-40) == -40). Run it. - Create weather.py that imports convert and prints the Fahrenheit value of a −18 °C February morning and a 60 km/h wind in metres per second.
- Create sizes.py that uses
from convert import cm_to_in to print a 180 cm person's height in inches, rounded to 1 decimal place. - Now make a folder tools with an empty
__init__.py, move convert.py into it, and fix the imports in the other two files (from tools import convert). - Finally, save an empty file named statistics.py in the folder, write
import statistics and print(statistics.mean([2, 4])) in another file, and observe the error. Delete statistics.py afterwards.
What to look for: −18 °C is −0.4 °F, 60 km/h is about 16.7 m/s, 180 cm is about 70.9 in; the self-test prints nothing when it is imported; step 5 gives an AttributeError because the empty statistics.py shadows the library.
Your turn
1What is the difference between a module and a package?
A module is one .py file. A package is a folder of modules marked by an __init__.py file.
2Write the import line that lets you call stats.mean([2, 4]) using the standard-library module statistics. What does the call return?
import statistics as stats. It returns 3 (the mean of 2 and 4).
3A module shapes.py defines area(r). Show two different ways to call it from another file in the same folder.
import shapes then shapes.area(2); or from shapes import area then area(2).
4What is the value of __name__ inside timing.py (a) when you run timing.py directly, (b) when report.py imports it?
(a) "__main__" (b) "timing".
5Why should you avoid from timing import *?
It copies every public name into your file, which can silently overwrite your own names, and readers cannot tell where a name came from.
6A student's program fails with AttributeError: module 'random' has no attribute 'randint'. What is the most likely cause?
There is a file named random.py (their own) in the same folder, which shadows the standard-library module. Rename it.
7Where must app.py be saved so that from carnival import scoring works?
In the folder that contains the carnival package folder (the script's own folder is searched first), not inside carnival.
More practice: warm-up
1What file extension does a Python module have?
2What file turns a folder into a package?
3Where do import statements usually go?
4What keyword gives a module an alias?
5Does a module's code run again every time it is imported?
No. It runs the first time; later imports reuse the same module object.
More practice: on your own
1Predict the output when main.py is run.
helper.py:
print("loading helper")
def greet(name):
return "Hi " + name
main.py:
import helper
import helper
print(helper.greet("Sam"))
loading helper (once only) then Hi Sam. The second import reuses the module that is already loaded.
2Rewrite helper.py from the previous question so that importing it prints nothing, but running it directly still prints loading helper.
def greet(name):
return "Hi " + name
if __name__ == "__main__":
print("loading helper")
3Two modules, bank.py and library.py, both define load(). Why is there no clash if main.py uses import bank and import library?
Each module has its own namespace, so the calls are bank.load() and library.load(), two different names. A clash would only happen with from bank import load and from library import load, where the second overwrites the first.
4Your package is named carnival. Write the line in app.py that imports the function points_for from the module scoring so you can call it without a prefix.
from carnival.scoring import points_for
5A module contains def _round_half(x): and def average(values):. What does the underscore tell another programmer?
_round_half is an internal helper, not part of the module's public interface; it may change, so other programs should use average and not rely on _round_half.
Challenge
1main.py contains only import a. a.py contains import b followed by def f(): return 1 (on two lines). b.py contains from a import f followed by print(f()). Running main.py stops with ImportError: cannot import name 'f'. Explain why, and how to restructure.
This is a circular import. main.py starts running a.py. Its first line imports b, so b.py starts running before a.py has reached its def. b asks for f from module a, which is only half-finished (it is already in Python's module cache, but f does not exist yet), so the import fails. Fix: move the shared code (f) into a third module, such as c.py, that both a and b import, or merge the modules. Circular imports usually signal a design problem: two modules that depend on each other should share a common lower-level module instead.
2Explain why from timing import format_time followed later by def format_time(s): return str(s) in the same file is a bug waiting to happen.
The def rebinds the name format_time in your namespace, replacing the imported function. Any call after that uses your version, which gives different output ("65" instead of "1:05"). With import timing and timing.format_time, there is no clash.
3Design the module layout for a school library app with these jobs: loading and saving the catalogue file, searching books, checking out and returning, and a text menu. Name the package and modules, and say which module(s) may print.
One good answer: a package library/ with __init__.py, storage.py (load_catalogue, save_catalogue), search.py (find_by_title, find_by_author), loans.py (check_out, return_book) and menu.py (show_menu, run). A top-level main.py calls menu.run(). Only menu.py prints or reads input; storage reads and writes files; search and loans are pure logic that return values or raise errors, so they can be tested and reused (for example by a future web version).
Check your understandingQuiz
1. Which statement imports the timing module so that you must write timing.format_time(65)?
2. What does if __name__ == "__main__": check?
3. What makes a folder a Python package?
4. A student saves a file called random.py in their project and then import random stops working. What is this problem called?
5. Why is from module import * discouraged?
6. A module is imported by three different files in one program. How many times does its top-level code run?
For parents and teachersBig idea: Expectation A2.1 asks students to create a modular program divided among multiple files (classes, libraries, modules). This lesson covers modules, packages and imports; the next lessons add classes. C1.4 (reusability) is the reason for all of it: code written once, used many times.
How to help: The activity needs a real folder on a computer, because multi-file programs cannot be shown in a single box. Some browser-based Python sites support only one file; if so, use a desktop editor such as Thonny or IDLE (both free). Ask your teen to show you the folder and explain which file imports which.
Common difficulties:
- Saving files in different folders, so imports fail with
ModuleNotFoundError. - Naming a file after a library module (random.py, statistics.py, turtle.py).
- Forgetting the
if __name__ == "__main__": guard, so test output appears whenever the module is imported. - Running a file from inside the package folder instead of from the project folder.