Modules and Environments
A module is a .py file; a package is a directory of them. Understanding how Python finds and loads those files — and how to isolate the third-party ones per project — is the difference between a project that works everywhere and one that works on your laptop.
Modules and import
Section titled “Modules and import”Any .py file is a module. Importing it executes it top to bottom, once, and caches the result in sys.modules.
GREETING = "Hello"
def greet(name): return f"{GREETING}, {name}!"
print("greetings module loaded")import greetings
greetings.greet("Ada") # => 'Hello, Ada!'greetings.GREETING # => 'Hello'Running python3 main.py prints greetings module loaded once, no matter how many times other modules import it.
Import forms
Section titled “Import forms”import json # bind the module nameimport numpy as np # bind under an aliasfrom pathlib import Path # bind one name from the modulefrom pathlib import Path as Pfrom collections import defaultdict, Counterfrom . import sibling # relative: same packagefrom .models import User # relative: submodule of this packagefrom ..utils import helper # relative: parent packageAll forms execute the entire module. from x import y does not load “less”; it just binds a different name.
__all__ = ["public_function"] # what `from mymodule import *` exports__all__ also documents a module’s intended public API, and some tools honour it.
Relative vs absolute imports
Section titled “Relative vs absolute imports”Absolute imports name the full path from a top-level package: from myapp.models import User. Relative imports use leading dots and are resolved from the current module’s package.
PEP 8 prefers absolute imports; they are unambiguous and survive files being moved. Relative imports are reasonable inside a self-contained package where the internal layout is an implementation detail.
Circular imports
Section titled “Circular imports”If a.py imports b and b.py imports a, one of them will see a half-initialised module and fail with ImportError or AttributeError. Fixes, in order of preference:
- Move the shared code into a third module both can import.
- Import inside the function that needs it, deferring the load.
- Use
import arather thanfrom a import thing— module-level attribute lookup happens later.
Packages
Section titled “Packages”A package is a directory Python treats as a module namespace.
myapp/├── __init__.py├── main.py├── models.py└── utils/ ├── __init__.py └── text.pyimport myapp.utils.textfrom myapp.utils.text import slugify__init__.py runs when the package is first imported. It can be empty (marking the directory as a package) or re-export a curated API:
from .models import User, Account
__version__ = "1.0.0"__all__ = ["User", "Account"]That lets consumers write from myapp import User regardless of your internal file layout.
Keep __init__.py cheap. Heavy work there slows every import of the package and creates circular-import risk.
if __name__ == "__main__"
Section titled “if __name__ == "__main__"”Every module has a __name__. It is "__main__" when the file is run directly, and the module’s dotted name when it is imported.
def main(): print("running")
if __name__ == "__main__": main()python3 tool.py # prints "running"import tool # prints nothingWithout the guard, importing the module would execute the script body — which is why every module intended to be both importable and runnable uses it. It also matters for multiprocessing on Windows and macOS, which re-imports the main module in child processes and will fork-bomb without the guard.
__main__.py in a package makes the package itself runnable:
myapp/├── __init__.py└── __main__.pypython3 -m myappThe import search path
Section titled “The import search path”import x searches sys.path, a list of directories, in order, and stops at the first match.
import sysfor p in sys.path: print(p)sys.path is built from:
- The directory of the script being run (or the current directory for
-c,-m, and the REPL). PYTHONPATHenvironment variable entries.- Installation-dependent defaults, including the active environment’s
site-packages.
import jsonjson.__file__ # where it was actually loaded fromFor finding your own package, do not mutate sys.path at runtime. Install the project in editable mode instead (below), which is the supported mechanism.
pip installs packages from PyPI, the Python Package Index. Always invoke it through the interpreter you mean to install into:
python3 -m pip install requestspython3 -m pip install "requests>=2.31,<3"python3 -m pip install -r requirements.txtpython3 -m pip install -e . # editable install of the local projectpython3 -m pip uninstall requestspython3 -m pip listpython3 -m pip show requests # version, location, dependenciespython3 -m pip freeze # exact versions, requirements.txt formatVersion specifiers:
| Specifier | Meaning |
|---|---|
requests |
Any version |
requests==2.31.0 |
Exactly this |
requests>=2.31 |
At least this |
requests~=2.31.0 |
>=2.31.0, <2.32.0 — compatible release |
requests>=2.31,<3 |
An explicit range |
Virtual environments
Section titled “Virtual environments”A virtual environment is a directory with its own site-packages and its own interpreter symlink. Activating it puts that interpreter first on PATH, so installs and imports are scoped to one project.
Why it is not optional:
- Two projects needing different versions of the same library would otherwise conflict.
- Installing globally can break OS tooling that depends on the system Python. Most Linux distributions now refuse global
pip installoutright (PEP 668,error: externally-managed-environment). - The environment is disposable — delete and rebuild it when things get strange.
python3 -m venv .venv # create it
source .venv/bin/activate # Linux / macOS.venv\Scripts\activate # Windows (cmd).venv\Scripts\Activate.ps1 # Windows (PowerShell)
python3 -m pip install requestswhich python # => /path/to/project/.venv/bin/python
deactivate # leave itAdd .venv/ to .gitignore. The environment is a build artifact — requirements.txt or pyproject.toml is the thing you commit.
Alternatives you will encounter: uv (a very fast Rust-based installer and environment manager that also handles Python versions), Poetry and PDM (project managers with lockfiles), conda (which manages non-Python dependencies too, common in scientific work), and pipx (installs command-line tools into isolated environments so they land on PATH without polluting anything).
pipx install ruff # a CLI tool, isolated, globally availablerequirements.txt vs pyproject.toml
Section titled “requirements.txt vs pyproject.toml”requirements.txt is a flat list of packages to install. Simple, universally understood, and the right tool for pinning a deployable application.
requests>=2.31,<3rich==13.7.0-r requirements-dev.txtpython3 -m pip install -r requirements.txtpython3 -m pip freeze > requirements.txt # snapshot exact versionspyproject.toml (PEP 518/621) is the modern, standard project file. It declares metadata, dependencies, and build configuration in one place, and most tools read their settings from it.
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"
[project]name = "myapp"version = "1.0.0"description = "An example application"requires-python = ">=3.12"dependencies = [ "requests>=2.31,<3", "rich>=13",]
[project.optional-dependencies]dev = ["pytest>=8", "ruff", "mypy"]
[project.scripts]myapp = "myapp.main:main" # creates a `myapp` command on install
[tool.ruff]line-length = 100python3 -m pip install -e . # install the project, editablepython3 -m pip install -e ".[dev]" # plus the dev extrasUse pyproject.toml for anything you package or distribute, and for any project that wants tool configuration in one file. Use requirements.txt for a simple script’s dependency list or as a generated lock file alongside it.
A conventional layout for a real project:
myproject/├── pyproject.toml├── README.md├── .gitignore├── src/│ └── myapp/│ ├── __init__.py│ ├── __main__.py│ └── models.py└── tests/ └── test_models.pyThe src/ layout prevents the project directory itself being on sys.path during tests, so you test the installed package rather than accidentally importing the source tree — which catches packaging mistakes early.
python -m recap
Section titled “python -m recap”-m resolves a name through the import system and runs it as __main__, using the interpreter you invoked. It is the correct way to run tooling and package entry points.
python3 -m venv .venvpython3 -m pip install xpython3 -m pytestpython3 -m http.server 8000python3 -m json.tool file.jsonpython3 -m zipfile -c archive.zip files/python3 -m timeit -s "import math" "math.sqrt(2)"python3 -m myapp # runs myapp/__main__.pypython3 -m myapp.tools.migrate # runs a submodule, relative imports intactKey points
Section titled “Key points”- Importing runs a module once and caches it in
sys.modules. - Prefer absolute imports; never use
import *. __init__.pymakes a package and defines its public surface — keep it light.if __name__ == "__main__":separates “runnable” from “importable”.sys.pathdecides what gets imported; a local file named after a stdlib module shadows it.- Use
python3 -m pip, and always inside a virtual environment. .venv/is disposable and gitignored; the dependency declaration is what you commit.pyproject.tomlis the standard project file;requirements.txtis still fine for pinning an application.