MA-PY1 · Module 4.2
Modules, imports, and the `__main__` guard
Topics: import · packages · __name__ · entry points
Overview
Module 4.2 — Modules, imports, and the __main__ guard
Scroll Core ideas → worked snippets → Practice (quiz). Attempt each question before Discuss / reveal. Use any Python REPL, script, or notebook you like.
Concepts
Core ideas — scaling code across files
A module is any .py file the import system loads once (per process) into sys.modules. A package is a directory of modules surfaced via package.submodule (modern Python supports namespace packages without __init__.py, but many tutorials still drop an __init__.py marker for clarity—both patterns exist.)
Import shapes
| Form | Behaviour |
|------|-----------|
| import math | Binds math name referencing standard module namespace. |
| from pathlib import Path | Imports Path symbol into importer’s global namespace—watch name clashes. |
| import numpy as np | Alias shortening—cultural convention heavy in scientific stacks. |
| from pkg import * | Discouraged in application code (wildcard hides origin, weakens tooling). Allowed if pkg.__all__ curated. |
Circular imports (A imports B imports A) cause ImportError / partially initialised modules symptoms—fixes: factor shared bits to third module, use lazy import inside functions, or redesign layering.
__name__, __package__, __spec__
At import time each module exposes __name__ equal to "__main__" only when invoked as python script.py or python -m runnable_pkg top-level shim semantics differ slightly—in scripts launched as __main__, guarded blocks run.
Idiom guarding executable demo code:
if __name__ == "__main__":
cli()
This prevents import toolbox from unintentionally triggering heavy benchmarks.
Executable modules (-m)
python -m json.tool runs package json.tool module as script—helps relative imports behave because sys.path[0] becomes current directory context properly for packages.
Contrast python pkg/mod.py which can subtly break import sibling resolving—prefer python -m pkg.mod inside project roots.
PYTHONPATH, pip, editable installs (awareness layer)
pip install -e path(editable) maps development tree intosite-packagesproxy simplifying multi-module projects (details belong in packaging docs).uv/poetry/venv tooling isolates deps—always teach virtual environment per project.
Self-study checklist
- Convert one throwaway
scriptintolib + __main__.pyrunnable package skeleton conceptually—even if filesystem exercise only. - Decide when
from x import yreadability beatsimport xverbosity. - Articulate one drawback of
import *.
Examples
Worked snippets — modules & entry points
A two-file walkthrough you can actually run
Let's build the smallest possible multi-file program and watch the import machinery work. Make a folder and put two files in it:
inventory/
pricing.py # reusable logic
report.py # the program a person runs
First, pricing.py holds a small reusable function and nothing that runs on its own:
# pricing.py
def with_tax(amount, rate=0.2):
return round(amount * (1 + rate), 2)
Now report.py imports that function and uses it. The from pricing import with_tax line tells Python: "find a module named pricing, and pull the name with_tax into this file."
# report.py
from pricing import with_tax
def main():
basket = [12.50, 4.00, 9.99]
for item in basket:
print(item, "->", with_tax(item))
if __name__ == "__main__":
main()
Run it from inside the inventory/ folder:
$ python report.py
12.5 -> 15.0
4.0 -> 4.8
9.99 -> 11.99
Why does this work? When Python runs report.py, it needs to locate pricing. It searches a list of folders called sys.path; the directory of the script being run is on that list, so the sibling file pricing.py is found. (If you moved pricing.py elsewhere, the import would fail with ModuleNotFoundError — the file has to be somewhere on sys.path.)
The __main__ guard in action. Notice report.py only calls main() when run directly. Import it instead and nothing prints:
$ python
>>> import report # loads the file, defines main — but prints nothing
>>> report.main() # now we ask for the output ourselves
12.5 -> 15.0
...
When you run report.py, its __name__ is the special string "__main__", so the guarded block fires. When you import it, __name__ is "report" instead, so the demo stays quiet and you just get the reusable main function. That single if is what lets one file be both a runnable program and an importable library.
You can also run it as part of a package, which makes package-relative imports behave: python -m inventory.report.
Re-import idempotency
import math
import math # cheap second time — resolves cache
math.pi
Behind scenes interpreter consults sys.modules.
Conditional demo execution
def triangle_numbers(n):
return [k * (k + 1) // 2 for k in range(1, n + 1)]
if __name__ == "__main__":
print(triangle_numbers(6))
Importing this file from elsewhere loads triangle_numbers without printing.
Guard against expensive import-time work
Avoid running network calls or gigantic data loads at module import top-level unless truly constant—defer into lazy_main().
Alias import
import itertools as it
print(list(it.islice(range(999), 3)))
Practice check — modules, imports, and __main__
Multiple-choice check from the Matheion app lesson. Pick an answer for each question, then submit.
1.Code under `if __name__ == '__main__':` runs in which situation?
2.After `import math`, how do you call the square-root function?
3.Why is `from module import *` generally discouraged?
4.Importing the same module a second time during one program run is cheap because…
5.What does running `python -m mypkg.tools` do?