Matheion

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 into site-packages proxy 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 script into lib + __main__.py runnable package skeleton conceptually—even if filesystem exercise only.
  • Decide when from x import y readability beats import x verbosity.
  • 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. 1.Code under `if __name__ == '__main__':` runs in which situation?

  2. 2.After `import math`, how do you call the square-root function?

  3. 3.Why is `from module import *` generally discouraged?

  4. 4.Importing the same module a second time during one program run is cheap because…

  5. 5.What does running `python -m mypkg.tools` do?