Split into modules
A big program in one file is hard to read and change. So we split it into modules: Python files, each with one clear job. main.py starts the program, notes.py manages notes and file_store.py saves them.
In main.py, the line import notes finds notes.py, runs it once and hands main.py a module object: a bundle of everything notes.py defines. Now main.py can use those names with a dot, like notes.add.
Public and hidden
notes.py offers two functions: add saves a note and show returns every note. They are its public interface, the ports on the block. Other modules use only these names and trust what they promise, without reading the code inside.
_clean tidies a note's text. Its leading underscore tells other programmers: internal, do not use from outside. __all__ lists the public names, so from notes import * skips _clean. It is a convention, not a lock.
Interfaces
notes.py must store notes somewhere, but should not care where. It accepts any object with two methods (functions attached to an object): save(text) and load(). That promise is an interface. The port has its shape: two holes.
file_store.py keeps the promise: its save writes each note as a line in notes.txt. Follow one call: main.py runs notes.add(s, "milk"), _clean turns it into "Milk", then s.save writes it to the file.
Unplug the file store and plug in db_store.py, which keeps the same promise with an SQLite database. Only lines 2 and 3 of main.py change. notes.py is not touched, and the program prints the same notes.
Good design
Coupling is how much one module depends on the inside of another. notes.py knows only save and load, so any store fits: low coupling. Had it opened notes.txt itself, moving to a database would mean rewriting notes.py too.
Cohesion is how well the parts inside one module belong together. Everything in notes.py is about notes; everything in db_store.py is about the database. Aim for high cohesion and low coupling: modules easy to understand, test and swap.
Look at who imports whom. main.py imports notes and one store. notes.py imports neither store: it depends only on its Storage interface, and each store is written to fit it. The details depend on the plan, never the reverse.
What happens
;; step 1/10 · modules module one .py file with one clear job main.py starts the program notes.py adds and shows notes file_store.py saves notes in a file db_store.py saves notes in a database a module's name is its file name without .py
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.