Skip to main content

Modules, packages & project layout

Level 3: Level 3: Patternsmedium18 minpackagesmodulesimportsproject-structure

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
Check yourself
store/__init__.py is an empty file. When does Python execute it?

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

Check yourself
catalog.py imports from cart.py while cart.py imports from catalog.py. You run the program. What do you actually see?

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.

Dependencies point one way
Step 1 / 3

Stage 1 of 3: The script or test you actually run, started from the project root so 'store' is importable.

Add one edge back from catalog to cart and this becomes a cycle. Whichever module Python happens to load second then sees the other half-built, which is why the error is an AttributeError on a name that plainly exists in the file.

Read the arrows as "imports from". A healthy package is a graph you can walk in one direction and always reach a leaf.

Check yourself
You are sitting in the project root, the folder that contains store/. cart.py opens with 'from store.catalog import price_of'. You run: python store/cart.py. What happens?

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.

Check yourself

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.

from store.catalog import price_of
from .catalog import price_of
from catalog import price_of
import store.catalog, then call store.catalog.price_of(name)
from ..store.catalog import price_of
Worked example (Python)
# 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"))   # 0

Apply

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.