JEPA4Japan · tutorials

Modules, Packages, and Virtual Environments

1,132 words 6 min read #Python

Split code across modules, control imports, and isolate project dependencies.

Course progress Course outline 24 of 24 lessons available

Put code in labeled drawers

One long Python file is like one giant toy box: everything is there, but nothing is easy to find. Python lets us give each job a labeled drawer.

  1. Module one importable .py file
  2. Import package a folder of modules
  3. Distribution something pip can install
  4. Virtual environment one project's Python home
Files organize code; environments organize the interpreter and installed distributions.

These labels are related, but not interchangeable. import loads a module or import package. python -m pip install ... installs a distribution. One distribution can provide several import packages, and its install name need not match its import name.

Import without spilling names

import statistics binds one module object. The dot shows where each tool came from:

import statistics

minutes = [20, 30, 40]
print(statistics.mean(minutes))
print(statistics.median(minutes))

Output:

30
30

This module prefix is a namespace. It lets your own name mean coexist with statistics.mean. A focused import is also possible:

from statistics import mean as average

print(average([10, 20, 30]))

Use aliases only when they clarify meaning. Avoid from statistics import *: a wildcard hides which names arrived and which old names they replaced.

The first successful import of a module in one Python process runs its top-level code from top to bottom. Later ordinary imports reuse the cached module. Top level should therefore mostly define functions, classes, and constants—not ask for input or start the program.

  1. First import run top-level definitions
  2. Remember cache the module object
  3. Main guard run app work only on purpose
Import makes definitions available; a main guard protects script-only work.
def main():
    print("Study report started.")


if __name__ == "__main__":
    main()

When run as the entry file, __name__ is "__main__", so the message appears. When imported, __name__ is the module’s import name, so main() becomes available without running.

Give a package one front door

A regular package is a directory containing __init__.py and related modules:

project/
├── study_report/
│   ├── __init__.py
│   ├── calculations.py
│   └── __main__.py
└── tests.py

__init__.py marks this regular package and runs on the first package import. It may be empty, or it may re-export a small public interface. Inside the package, one leading dot means “from this package”:

from .calculations import summarize

__all__ = ["summarize"]

__all__ documents an intended interface; it is not a privacy or security wall. Avoid circular imports: if A imports B and B imports A, move shared work to a lower-level module or pass values across a clearer boundary.

  1. Import __init__.py opens the API
  2. Relative import the dot keeps package context
  3. python -m runs __main__.py
A package has an import door and an optional run button.

Run a package from the directory that contains it:

python -m study_report

Do not run python study_report/__main__.py; that treats the inner file as a loose script and can break relative imports.

Build a runnable study-report package

Create the tree above. Put this in study_report/calculations.py:

def summarize(sessions):
    totals = {}
    for topic, minutes in sessions:
        totals[topic] = totals.get(topic, 0) + minutes
    return sorted(totals.items())

Put this in study_report/__init__.py:

from .calculations import summarize

__all__ = ["summarize"]

Put this in study_report/__main__.py:

from . import summarize


def main():
    sessions = [("Python", 45), ("Git", 40), ("Python", 30)]
    print("Study report")
    for topic, minutes in summarize(sessions):
        print(f"- {topic}: {minutes} minutes")


if __name__ == "__main__":
    main()

Finally, put this in tests.py beside the package:

from study_report import summarize

assert summarize([]) == []
assert summarize([("Python", 20), ("Python", 30)]) == [("Python", 50)]
print("Tests passed.")

Run python tests.py, then python -m study_report. Expected output:

Tests passed.
Study report
- Git: 40 minutes
- Python: 75 minutes

The test import prints no report. The package exposes one stable operation, while __main__.py owns visible app behavior.

Check the source and give each project a home

A local file named statistics.py, json.py, or pathlib.py can shadow the real library. Inspect what Python loaded:

import statistics

print(statistics.__name__)
print(statistics.__file__)

The second line is machine-specific; verify that it points to the expected library, not your project. Fix the filename or layout instead of scattering edits to sys.path.

Create an isolated environment from the project root:

python -m venv .venv
source .venv/bin/activate
python -c "import sys; print(sys.prefix != sys.base_prefix)"

Windows PowerShell uses .venv\Scripts\Activate.ps1. The check should print True. Install a needed distribution with python -m pip install distribution-name, then leave with deactivate. Add .venv/ to .gitignore and record the creation commands; recreate the environment instead of moving or committing it. A venv isolates installed distributions, not your data files or import design.

Three tiny missions and the handoff

  1. Safe import. Move a temperature function to conversions.py; keep display behind a main guard in weather_report.py. Confirm importing it prints nothing.
  2. New package. Make a reading_log package with one public summary function, an __init__.py, and a __main__.py. Run it with python -m reading_log.
  3. Environment proof. Create .venv, activate it, verify the prefix check is True, deactivate, and confirm .venv/ is ignored by Git.

Sharp corners: wildcard imports hide names, top-level work causes import side effects, direct execution can lose package context, and a local filename can load the wrong module.

  • I can distinguish a module, import package, distribution, and venv.
  • I can explain namespaces and first-import execution.
  • I can use a main guard and a small __init__.py interface.
  • I can run and test a package with python -m.
  • I can inspect an imported module’s source.
  • I can create, verify, leave, and recreate .venv.
  • I completed the three tiny missions.

Next, you will use pathlib, with, CSV, and JSON to give this organized code safe local storage.