Python from First PrinciplesPost 9 of 20 · 20 of 20 written
- How to use this guide
- 01What Python actually is
- 02Names, objects and references
- 03Built-in types and what they cost
- 04Control flow and expressions
- 05Functions in depth
- 06Iteration and generators
- 07Classes and the data model
- 08Context managers and exceptions
- 09Modules, packages, imports and environments
- 10Type hints and Pydantic
- 11The performance model
- 12Concurrency and parallelism
- 13NumPy: the array model everything else copies
- 14Data and serialization
- 15PyTorch mental model
- 16AI systems engineering in Python
- 17Tooling, debugging and testing
- 18Asking the right questions
- 19Roadmap and build-to-learn projects
- 20Glossary and resources
9. Modules, packages, imports and environments
import executes a file once, caches the resulting module object, and binds a name to it. Most “works on my machine” problems in AI are environment problems: the wrong interpreter, the wrong site-packages folder, or the wrong CUDA build.
What import foo does
- Check the cache,
sys.modules. Iffoois there, reuse it; the file is not run again. - Find the file by searching each directory in
sys.path: the script’s folder,PYTHONPATH, the standard library, then site-packages. - Execute the file’s top-level code inside a new module object’s namespace.
- Bind the name
fooin the importing namespace.
Consequences worth remembering:
- Top-level code runs at import. Loading a model at module level makes every import of that module slow, so put heavy work in functions.
- Module-level variables are singletons shared by every importer.
- Circular imports fail when module A uses B’s names before B has finished executing.
- A file named
torch.py,random.pyorjson.pyin your project shadows the real library. When imports behave strangely, printfoo.__file__.
if __name__ == "__main__":
A module’s __name__ is "__main__" when run as a script and its dotted name when imported. Guard your entry point so importing the file has no side effects:
def main():
...
if __name__ == "__main__":
main()The guard is mandatory with multiprocessing under the “spawn” start method (the default on macOS and Windows), because each worker re-imports your main module. PyTorch DataLoader workers count.
Packages and project layout
A package is a directory of modules, usually with an __init__.py that runs when the package is imported. A layout that avoids most import confusion:
my_project/
pyproject.toml
src/my_project/
__init__.py
data.py
model.py
serve.py
tests/
test_data.py- Absolute imports:
from my_project.data import load_dataset. - Relative imports inside the package:
from .data import load_dataset. - Run modules with
python -m my_project.serve. Runningpython src/my_project/serve.pydirectly often breaks relative imports.
Virtual environments
A virtual environment is a folder with its own link to an interpreter and its own site-packages. Each project gets its own dependency versions without touching the system Python.
- Create and activate:
python -m venv .venvthensource .venv/bin/activate. - Diagnose which environment you are in:
which pythonandpython -c "import sys; print(sys.executable)". - Install with
python -m pip install ..., never barepip, so pip belongs to the interpreter you are running.
Package management today
pyproject.tomlis the standard project file: metadata, dependencies and tool settings (ruff, pytest, mypy) in one place.- uv (from Astral) is a fast installer and project manager:
uv init,uv add torch,uv run script.py. It writes a lock file and can install Python versions too, and it has become a common default for new projects. - pip with
requirements.txtis still everywhere. Pin exact versions with==for reproducibility. - conda or mamba help when you need non-Python binaries, such as some CUDA toolkits or geospatial libraries.
Libraries declare version ranges (torch>=2.4,<3); applications lock exact versions and commit the lock file.
Wheels, native code and CUDA
- A wheel (
.whl) is a prebuilt package. AI libraries ship compiled C++ and CUDA code inside wheels tied to the OS, CPU architecture, Python version and often the CUDA version. - PyTorch publishes separate builds per CUDA version. Installing the wrong one gives you a CPU-only build or a driver mismatch. Verify with
torch.__version__,torch.version.cudaandtorch.cuda.is_available(). - The NVIDIA driver must support the CUDA runtime bundled in the wheel. PyTorch wheels usually do not need a separately installed CUDA toolkit.
- On Apple Silicon, the GPU backend is MPS: check
torch.backends.mps.is_available(). - Docker images pin the whole stack (OS, Python, wheels) so deployment matches development.
Questions to ask:
- Which interpreter and which site-packages is this actually running?
- Is anything heavy happening at import time?
- Is this PyTorch build compiled for a CUDA version this machine’s driver supports?