Projects and packages

Part of the free LogicWiz Python course, Module 15: Real-world Python. The examples and practice run in the app.

Canteen Counter is now one 900-line file called app.py, and every change breaks something else. Meher splits it into a folder of small files, runs it, and gets ModuleNotFoundError: No module named 'canteen'. Real projects are folders, not files. Let's learn how they fit together.

You'll learn to

  • Split a program into a package (a folder with `__init__.py`) and import from it
  • Explain how Python finds a module (`sys.path`) and why your own `random.py` breaks things
  • Use `pip`, `venv` and `requirements.txt` the way every real team does
  • Read command-line options with `argparse`, and add type hints to functions

Learn

A module is a file, a package is a folder

Any .py file is a module. import billing runs billing.py once and gives you its names as billing.gst, billing.GST_RATE and so on.

A package is a folder of modules with an __init__.py file inside. That file runs the first time anyone imports the package; it can be empty.

canteen_counter/
    main.py            <- you run this
    canteen/
        __init__.py    <- marks canteen as a package
        billing.py
        menu.py
    requirements.txt

From main.py you write from canteen.billing import gst or import canteen.menu as menu. The dot means "inside".

Importing from your own package

from canteen.billing import gst
import canteen.menu as menu
import canteen

bill = menu.price("Samosa") * 2 + menu.price("chai")
print("Subtotal:", bill)
print("With GST:", gst(bill))
print("Package version:", canteen.VERSION)

import canteen          # imported again: __init__.py does NOT run twice
print("done")

The folder already has canteen/__init__.py, canteen/billing.py and canteen/menu.py. Notice "canteen package loaded" prints only once: a module is run the first time it is imported, then reused.

Where does Python look? sys.path

import sys

print("Python searches these folders, in order:")
for folder in sys.path[:4]:
    print("  ", folder or "(the folder you ran the program from)")

import json
print("json was found at", json.__file__)
print("This program's own name is", __name__)

The first entry is your program's own folder. Then come the standard library and installed packages. Python takes the first match it finds.

Never name your file after a library. If Aarav saves his dice game as random.py, then import random finds his file first (his folder is first in sys.path), and random.randint gives AttributeError. The same happens with requests.py, pandas.py, math.py. Rename the file and delete the __pycache__ folder next to it.

pip, venv and requirements.txt (on a laptop)

Run these in a terminal, not in Python. Each project gets its own virtual environment, a private folder of libraries, so project A can use pandas 1.5 while project B uses pandas 2.2.

python -m venv .venv            # make the environment once
source .venv/bin/activate       # Windows: .venv\Scripts\activate
pip install requests pandas     # install into THIS project only
pip freeze > requirements.txt   # save exact versions

A teammate then runs pip install -r requirements.txt and gets the very same versions. Commit requirements.txt to git; never commit the .venv folder.

The browser runner already has the standard library, numpy, pandas and matplotlib, so here you just import them.

Command-line options with argparse

import argparse

parser = argparse.ArgumentParser(description="Canteen Counter: bill one item")
parser.add_argument("item", help="what the student ordered")
parser.add_argument("--qty", type=int, default=1, help="how many")
parser.add_argument("--veg", action="store_true", help="veg only")

# On a laptop you would type:  python bill.py Samosa --qty 3 --veg
# and call parser.parse_args() with no list. Here we pass the same words as a list:
args = parser.parse_args(["Samosa", "--qty", "3", "--veg"])
print(args)
print(args.item, args.qty, args.veg)
print(type(args.qty))

type=int converts the text "3" to the number 3 for you. store_true makes a flag: present means True, absent means False. sys.argv is the raw list of words; argparse turns it into named values and writes the --help page for free.

Type hints: labels, not locks

def split_bill(total: float, people: int) -> float:
    return round(total / people, 2)


def find_price(menu: dict[str, int], item: str) -> int | None:
    return menu.get(item)


print(split_bill(250, 4))
print(find_price({"chai": 10}, "chai"), find_price({"chai": 10}, "dosa"))
print(split_bill.__annotations__)

total: float says what you expect; -> float says what comes back. Editors and the free tool mypy use hints to catch mistakes before you run. Python itself ignores them at run time.

Interviewers love if __name__ == "__main__":. When you run a file directly, its __name__ is "__main__". When another file imports it, __name__ is the module's name (like "billing"). So code under that if runs only when the file is the program, never on import. Put your main() call there.

For style, PEP 8 is the rulebook (4-space indents, snake_case names), and tools like black and ruff fix and check it automatically.

In one line

A package is a folder with __init__.py; imports search sys.path in order; venv + requirements.txt pin libraries; argparse reads options; type hints are labels Python does not enforce.

Interview corner

What is `__init__.py` for?

It marks a folder as a regular package and runs once, the first time the package is imported. It can be empty, or it can set package-level names and re-export things (from .billing import gst) so users can write from canteen import gst. Since Python 3.3 a folder without it can still be imported as a namespace package, but normal projects include it.

What does `if __name__ == "__main__":` do?

Every module has a __name__. The file you run directly gets "__main__"; an imported file gets its module name. So code under that if runs only when the file is the program, not when it is imported, which lets one file be both a reusable module and a script. It is also required for multiprocessing on Windows and macOS.

Why use a virtual environment and `requirements.txt`?

A venv gives each project its own installed libraries, so two projects can need different versions without clashing and you never break the system Python. pip freeze > requirements.txt records exact versions, and pip install -r requirements.txt recreates them on any machine, which makes builds reproducible.

Does Python enforce type hints?

No. Hints like def f(x: int) -> str are stored in f.__annotations__ and ignored at run time. Static checkers (mypy, pyright) and editors use them to find bugs before running, and libraries like pydantic or FastAPI choose to read them for validation.