Python Modules, Packages and Virtual Environments
AI-assistedDrafted with AI assistance and reviewed before publishing; examples are version-pinned. Verify against official docs when in doubt.
As soon as a project outgrows one file, you split it into modules. As soon as it needs a third-party library, you isolate its dependencies. Both steps have a standard shape in Python.
Environment
Examples are verified on Python 3.13; venv and pip ship with the interpreter.
python -m pip --version
A module is a .py file
utils.py:
def slugify(text: str) -> str:
return "-".join(text.lower().split())
Import it from the same directory:
import utils # utils.slugify("Hello World")
from utils import slugify # slugify("Hello World")
import numpy as np # conventional alias
Prefer import module over from module import *: the star import pollutes the namespace and hides where a name came from.
How imports resolve
Python searches, in order:
- Modules already imported (
sys.modules) - The directory of the running script
PYTHONPATH- The standard library and
site-packages
import sys
print(sys.path)
Compiled bytecode is cached in __pycache__/, which belongs in .gitignore.
Packages
Any directory containing __init__.py is a package (namespace packages without it exist, but explicit is clearer):
myapp/
├── __init__.py
├── cli.py
└── utils.py
from myapp import utils # absolute import, preferred
from myapp.utils import slugify
Inside a package, relative imports are allowed: one dot is the current package, two dots the parent.
# myapp/cli.py
from .utils import slugify
from ..config import load
Relative imports only work while the module is imported as part of the package. Running python myapp/cli.py fails with ImportError: attempted relative import with no known parent package. Run it as a module instead:
python -m myapp.cli
The __main__ guard
# myapp/cli.py
def main() -> None:
print("run")
if __name__ == "__main__":
main()
When the file is executed directly, __name__ is "__main__" and main() runs. When another module imports it, the block is skipped. This is what makes a file usable both as a script and as a library.
Virtual environments
Each project gets its own dependency set, so versions cannot collide:
python -m venv .venv
source .venv/bin/activate # macOS / Linux
.venv\Scripts\activate # Windows
deactivate
After activation, python and pip point inside the environment:
which python # .../project/.venv/bin/python
pip install requests
Activation is a convenience, not a requirement. CI usually calls the interpreter directly:
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m myapp.cli
python -m pip is preferable to bare pip, because it guarantees the installer belongs to the interpreter you are using.
Dependency management
pip install requests==2.32.3 # pin a version
pip install -r requirements.txt
pip freeze > requirements.txt # exact versions of the current environment
pip list
pip show requests
requirements.txt:
requests==2.32.3
rich==13.9.4
Pinning with == makes installs reproducible, which is the only reliable cure for “works on my machine”.
Modern projects declare metadata and dependencies in pyproject.toml:
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = ["requests>=2.32"]
[project.scripts]
myapp = "myapp.cli:main"
With a [project.scripts] entry you get a myapp command after installation. For development, install the project itself in editable mode:
python -m pip install -e .
.gitignore at minimum:
.venv/
__pycache__/
*.pyc
For applications, also commit a lock file (uv.lock, poetry.lock, or a fully pinned requirements.txt). For libraries, keep ranges in pyproject.toml so downstream projects can resolve compatible versions.
Common mistakes
1. Installing into the global environment
Forgetting to activate the venv installs into the system interpreter, where permissions and version conflicts await. Check the prompt or which python before installing.
2. Naming a file after a standard library module
json.py, email.py, random.py shadow the real modules and produce baffling AttributeErrors. Rename the file.
3. Circular imports
a.py imports b, which imports a. Move shared code into a third module, or defer the import inside a function.
4. Relative imports in script mode
Use python -m package.module, or switch to absolute imports.
5. Committing .venv
Virtual environments contain absolute paths and are not portable. Commit the dependency specification instead.
6. Editing installed packages
Changes under site-packages vanish on the next install. Use editable installs (pip install -e .) during development.
Summary
- A
.pyfile is a module; a directory with__init__.pyis a package. - Prefer absolute imports; relative imports belong inside packages, run via
python -m. if __name__ == "__main__":separates “run” from “import”.- One venv per project; pin versions in
requirements.txtorpyproject.toml.
Next: classes, methods, and Python’s data model.