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:
- Look in
sys.modules, the cache. Ifxis there, use it. - Otherwise search
sys.path(the script's directory, thenPYTHONPATH, the standard library, thensite-packages). - 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, orrequests.pyin your project shadows the real module, because the script's directory comes first insys.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.