Modules, packages & project layout
Split logic across a real Python package with an __init__.py and cross-module imports.
From one file to a package
One 400-line .py file is where a project goes to die: you scroll forever, everything can touch everything, and nothing tells you who depends on whom. Splitting logic into modules fixes that. Each file owns one responsibility, and the imports at the top of a file become a readable map of its dependencies. This is the single most common way real Python codebases stay navigable, and interviewers notice when you reach for it.
Modules and packages
A module is a single .py file. When you import it, Python runs the file top to bottom once and hands you a namespace object whose attributes are the names defined inside.
A package is a directory of modules with an __init__.py file (it may be empty). That file marks the folder as importable and runs the first time the package is imported.
store/
__init__.py # marks 'store' as a package
catalog.py # owns prices + lookups
cart.py # depends on catalog
Importing across modules
Inside cart.py, reach a sibling module by its package-qualified path:
# store/cart.py
from store.catalog import price_of
def cart_total(names):
return sum(price_of(name) for name in names)
# a test or app, run from the project root
from store.cart import cart_total
print(cart_total(["apple", "bread"])) # 5
from store.catalog import price_of is an absolute import, spelled from the project root. Inside a package you can also write the relative form from .catalog import price_of, where the leading dot means "this package". Relative imports only work inside a package, not in a file you run directly as a script.
Pitfalls
Circular imports. If catalog imports from cart while cart imports from catalog, whichever module loads second sees the first one only half-built, and you get an ImportError or AttributeError. The fix is to point dependencies one way. Here cart depends on catalog, never the reverse.
Stage 1 of 3: The script or test you actually run, started from the project root so 'store' is importable.
Read the arrows as "imports from". A healthy package is a graph you can walk in one direction and always reach a leaf.
Running a package file directly. python store/cart.py fails with ModuleNotFoundError: No module named 'store', because running a file puts its own folder (store/) on the import path instead of the project root, so store is not importable. Run it as a module from the project root with python -m store.cart, or import it from a top-level script instead. (Had cart.py used the relative from .catalog import price_of, the same command would fail differently, with attempted relative import with no known parent package.)
Interview nuance: a module is a singleton. The first import runs the file body and caches the resulting module object in sys.modules; every later import returns that same cached object without re-running the file. So top-level code (a PRICES dict, a database connection) executes exactly once per process, and any module-level state is shared everywhere it is imported. Interviewers probe this when they ask why an import side effect runs only once, or how two modules end up mutating the same object.
You are editing store/cart.py and you need price_of from store/catalog.py. The program is started from the project root with: python -m store.cart. Sort each import line by whether it resolves.
# one file now; a package next
PRICES = {"apple": 3, "bread": 2}
def price_of(name):
return PRICES.get(name, 0)
print(price_of("apple")) # 3
print(price_of("candy")) # 0Apply
Your turn
The task this lesson builds to.
Warm-up (one file): implement cart_total(prices, names). Total the price of every name in
names, looking each one up in the prices dict (missing items cost 0).
For prices = {"apple": 3, "bread": 2} and names = ["apple", "bread"], return 5.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Now build it as a package. Implement cart_total(names) in store/cart.py using the
read-only price_of helper imported from store.catalog. Unknown items cost 0. Open the visible
test to see expected behaviour; some tests are hidden.
3 hints and 2 automated checks are waiting in the workspace.