Modules and interfaces
A big program is easier to read, test and change when it is split into modules: files that each do one job. Follow a small notes program in Python as main.py imports notes.py, uses only its public functions, and never sees its hidden helper. Then unplug a file store and plug a database store into the same Storage interface: two lines of main.py change, notes.py does not change at all, and the output stays the same.
5 quick questions
Pick an answer to see why. Your score saves with your progress.
01What happens the first time main.py runs the line import notes?
import finds the module, runs its code once and binds the module object to the name notes. Later imports of notes reuse the same object instead of running the file again.
02notes.py defines _clean, and __all__ = ["Storage", "add", "show"]. What does that tell other programmers?
A leading underscore marks a name as internal, and __all__ lists the public names (it is what from notes import * imports). Both are conventions: Python still lets you call notes._clean, but you should not depend on it.
03Why can db_store.py replace file_store.py without any change to notes.py?
notes.py depends only on the Storage interface: an object with save(text) and load(). Any store that keeps that contract fits the same port, so only main.py, which picks the store, changes.
04Which design has low coupling?
Coupling is how much one module depends on another's insides. Talking only through save and load means the store can change freely; opening notes.txt directly would tie notes.py to files.
05A module called utils.py holds code for dates, emails, colours and passwords. What is its problem?
Cohesion is how closely the parts of one module belong together. A module that mixes unrelated jobs is hard to name, understand and change. Split it into modules with one job each.
Glossary
- Module
- One Python file (.py) of definitions with one clear job. Its name is the file name without .py.
- import
- The statement that finds a module, runs it the first time, and makes its names usable as module.name.
- Public interface
- The names a module offers to others (in notes.py: Storage, add and show), and what they promise to do.
- Internal (private) name
- A name that starts with an underscore, such as _clean. By convention, only the module itself uses it.
- __all__
- A list of a module's public names. from module import * imports exactly these.
- Interface (contract)
- A promise about what an object can do, such as having save(text) and load() methods. Any object that keeps it can be used.
- Coupling
- How much one module depends on the details of another. Low coupling makes changes stay local.
- Cohesion
- How closely the parts inside one module belong together. High cohesion means one module, one job.
Notes and sources
- Python checks none of this while the program runs: any object with save and load works (duck typing). The Storage Protocol class is there for people and for type checkers such as mypy, which compare a store against it.
- The underscore and __all__ are conventions. Python still lets you write notes._clean(...) from outside; they tell other programmers what is safe to rely on.
- import notes looks for notes.py on the module search path (sys.path), which starts with the folder of the script you run. The module runs only on the first import; later imports reuse it from sys.modules.
- The five files on this page are real Python 3. Run main.py with either store and it prints ['Milk'].
- Python Tutorial: Modules
- Python Language Reference: The import system (the module cache)
- Python Language Reference: The import statement (__all__ and public names)
- PEP 8: Public and internal interfaces
- Python Library Reference: typing.Protocol
- PEP 544: Protocols, structural subtyping (static duck typing)
- Python Library Reference: sqlite3
- D. L. Parnas, On the criteria to be used in decomposing systems into modules, CACM 15(12), 1972
- W. P. Stevens, G. J. Myers, L. L. Constantine, Structured design, IBM Systems Journal 13(2), 1974
Quiz Unlocks after the last step.