Virtual environments and dependency pinning in real projects. The beginner tutorials introduced venvs; here we treat them as non-optional for anything you might deploy or share.
One project, one venv
A virtual environment is an isolated Python with its own installed packages. Use one venv per project so dependency versions do not bleed together.
cd myproject
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
Add .venv/ to your .gitignore. Commit the instructions to recreate the environment, not the environment itself.
requirements.txt for pinning
After installing what you need, freeze exact versions to a requirements file.
pip install requests flask pytest
pip freeze > requirements.txt
A fresh clone should be able to run:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
Pin deliberately, not accidentally
pip freeze captures everything in the venv, including tools you installed once and forgot. For libraries you publish, prefer declaring direct dependencies in pyproject.toml and generating a lock file with a tool like pip-tools when the project grows.
# requirements.in (top-level deps only)
flask>=3.0,<4
requests>=2.31
# compile with: pip-compile requirements.in
Checking you are in the right venv
When imports fail mysteriously, you are often using the system Python by mistake.
which python
python -c "import sys; print(sys.prefix)"
pip list
Dev dependencies separately
Test runners and linters belong in development dependencies, not in production installs when you can help it.
# pyproject.toml excerpt
[project.optional-dependencies]
dev = ["pytest>=8.0", "ruff>=0.4"]
# install with:
pip install -e ".[dev]"
Reproducible builds matter
‘It worked on my machine’ usually means unpinned versions. Pinning feels fussy until the day a minor release breaks your app on a Friday afternoon. Then it feels like insurance.
Next up: OOP patterns you will actually use – dataclasses, composition, and a few dunder methods without Java cosplay.

