Frontend Labs
⋯

9. Modules, packages, and imports

9.1 Modules and the import system

A module is a .py file. A package is a directory of modules (usually with an __init__.py). import x does this, once per process:

  1. Look in sys.modules, the cache. If x is there, use it.
  2. Otherwise search sys.path (the script's directory, then PYTHONPATH, the standard library, then site-packages).
  3. Create a module object, execute the file's top-level code, and store the module in sys.modules.
import sys
print(sys.path[:3])
import json
print(json.__file__)        # where it came from
print("json" in sys.modules)

Step 3 has consequences:

  • Top-level code in a module runs on import. Keep it to definitions and constants; no network calls or heavy work at import time.
  • Name your files carefully. A file called random.py, email.py, or requests.py in your project shadows the real module, because the script's directory comes first in sys.path. The error message (AttributeError: module 'requests' has no attribute 'get') does not say why.

9.2 Import styles

import json                         # use as json.dumps
import numpy as np                  # conventional aliases: np, pd, plt, torch
from pathlib import Path            # import a name directly
from collections import Counter, defaultdict
# from os import *                  # never: you cannot tell where names come from

9.3 A package layout

py-labs/
├── pyproject.toml
├── src/
│   └── netkit/
│       ├── __init__.py        # runs on `import netkit`; can re-export the public API
│       ├── dns.py
│       ├── http.py
│       └── _util.py           # leading underscore: private by convention
└── tests/
    └── test_dns.py
# src/netkit/http.py
from ._util import parse_headers     # relative import inside the package
from .dns import resolve

Python has no private keyword. A leading underscore (_util, _internal_fn) is the convention for "not part of the public API". Define __all__ in a module to list its public names. Chapter 21 turns this layout into an installable package.

9.4 __name__ == "__main__" and python -m

When Python runs a file directly, that module's __name__ is "__main__"; when imported, it is the module's name. Guarding main() lets one file be both a library and a script. python -m pkg.module runs a module as a script with the package on the path, which makes relative imports work. Prefer python -m (or uv run -m) to python path/to/file.py inside packages. Standard library modules work the same way:

python -m http.server 8000     # a static file server
python -m json.tool data.json  # pretty-print JSON
python -m venv .venv
python -m timeit "sum(range(1000))"

9.5 Circular imports

If a.py imports b.py and b.py imports a.py at the top level, one of them sees a half-initialized module and you get ImportError: cannot import name 'X' from partially initialized module. Fix it by moving shared code into a third module, or by importing inside the function that needs it. A circular import is usually a design smell: two modules that need each other are one module, or are missing a third.


Generated from real-life-python-guide.md · 8 Oct 2026 16:36