BeginnerModules, packages, and environments~30 minPython

Modules, Packages, and Environments

Import code across files correctly, understand what the if __name__ == "__main__" guard actually does, and keep dependencies isolated with a virtual environment.

By the end of this lesson, you can

  • Explain what __name__ is set to when a file runs directly versus when it's imported
  • Write the if __name__ == "__main__" guard correctly, and explain what it protects against
  • Distinguish a virtual environment's isolated packages from a system-wide install
  • Evaluate a third-party package responsibly before adding it as a dependency

Why it matters

Every example so far has ended with the same two lines — if __name__ == "__main__": main() — without a full explanation of what they do or why they matter. Once a program grows past one file, understanding that pattern (and where packages actually get installed) is what separates code that works only as a standalone script from code that’s genuinely reusable.

Mental model

Every module — every .py file — is a namespace: a mapping from names to the objects defined in it. import greeting_helpers doesn’t dump greeting_helpers’s names into the importing file’s own namespace; it makes the whole module available as one name, and greeting_helpers.greet(...) reaches inside it explicitly. This is exactly why a name like pi can exist inside math without ever colliding with an unrelated local variable also called pi.

Every module also has a __name__ variable, set automatically depending on how the file started running: "__main__" if it was executed directly, or the module’s own name if another file imported it instead. That’s the entire mechanism behind the guard every example on this site ends with.

Worked example: one file imports another

Pythongreeting_helpers.py
"""A tiny helper module — meant to be imported, not run directly."""


def greet(name: str) -> str:
    return f"Hello, {name}!"


if __name__ == "__main__":
    print("This module is meant to be imported, not run directly.")
Pythonmodules_and_environments.py
import math

import greeting_helpers


def main() -> None:
    print(greeting_helpers.greet("Ada"))
    print(f"pi is approximately {math.pi:.4f}")
    print(f"__name__ here is: {__name__!r}")


if __name__ == "__main__":
    main()
Outputpython modules_and_environments.py
Hello, Ada!
pi is approximately 3.1416
__name__ here is: '__main__'

Running modules_and_environments.py directly sets its own __name__ to "__main__", so its guard’s main() call runs. greeting_helpers is imported, not run directly — its own guard never fires, which is exactly why greeting_helpers.py’s “this module is meant to be imported” message never appears in the output above, even though that file’s code did execute (its def greet(...): ran, defining the function modules_and_environments.py then calls).

Common mistakes

  • Writing side-effecting code (a print, a file write, a network call) at module level, outside any function and outside the guard — it runs every time the file is imported, not just when run directly, which is rarely what’s intended.
  • Defining functions or classes inside the guard — this makes them unavailable to anything that imports the file, since the guard’s body never runs on import. Definitions go outside the guard; only the “do something now” call belongs inside it.
  • Installing packages globally (no active virtual environment) and being surprised later when two projects need incompatible versions of the same dependency.

Check your understanding

# In a file named tools.py: def helper(): return 42 print("tools.py is loading") if __name__ == "__main__": print(helper()) # Another file does: import tools # What prints, from the import alone?
Or reveal the answer without checking

Answer:"tools.py is loading"
The print("tools.py is loading") line is outside the guard, so it runs whenever the file is loaded at all — including by an import. The print(helper()) call is inside the guard, so it only runs when tools.py itself is executed directly, not when it's imported.

Two unrelated projects on the same machine need different major versions of the same third-party package. What's the standard fix?
Or reveal the answer without checking

Answer:Give each project its own virtual environment, so each can have the version it needs
A virtual environment isolates each project's installed packages from every other project's — this exact version-conflict scenario is the main reason virtual environments exist rather than installing everything system-wide.

Practice: warm-up

Predict, then check: if greeting_helpers.py (from the worked example) is imported twice from different files in the same program run, does its module-level code — including any print statements outside the guard — run twice?

Stuck? Reveal one hint at a time.

  1. Hint 1

    Python caches every module it imports, in a structure it consults before running a module's code again.

  2. Hint 2

    The second import (from anywhere in the same running program) finds the already-loaded module in that cache and reuses it directly.

  3. Hint 3

    No — a module only actually runs once per program execution, no matter how many different files import it. Every import after the first just retrieves the same, already-created module object.

Reveal the answer

Try the problem yourself before reading this. There is often more than one reasonable approach — treat this as one worked example, not the only correct answer.

File A: import greeting_helpers   # greeting_helpers.py's code runs, once
File B: import greeting_helpers   # reuses the already-loaded module — no re-run

Python caches every module by name the first time it’s imported (sys.modules), so a second import of the same module anywhere else in the same running program is a cache lookup, not a re-execution.

Practice: apply it

Turn a small script into two files: a stats.py module with a reusable mean(numbers: list[float]) -> float function, and a separate script that imports and calls it. Predict what happens if you accidentally define mean’s logic inside the if __name__ == "__main__": guard instead of at module level.

def mean(numbers): ... is defined inside if __name__ == '__main__': in stats.py by mistake. What happens when another file does import stats; stats.mean([1, 2, 3])?
Or reveal the answer without checking

Answer:An AttributeError, because mean was never defined at module level — only inside the guard, which never ran on import
A def inside the guard only executes (and so only creates the function) when the file runs as __main__. On import, the guard's body never runs, so stats.mean simply doesn't exist yet — accessing it raises AttributeError: module 'stats' has no attribute 'mean'.

Summary

  • Every module is its own namespace; import module_name makes that whole namespace available as one name rather than merging its contents into the importer’s own namespace.
  • __name__ is "__main__" when a file runs directly, and the module’s own name when another file imports it — the entire mechanism behind if __name__ == "__main__":. Definitions go outside the guard; only “run this now” code belongs inside it.
  • A module only actually executes once per program run, no matter how many files import it — Python caches already-imported modules.
  • A virtual environment isolates a project’s installed packages from the system Python and from other projects; evaluate a third-party package’s maintenance, license, and trustworthiness before adding it as a dependency.