Virtual Environments and pip Packages

8 min read

A virtual environment is an isolated Python install that lives inside your project. Packages installed there don't pollute the system Python or any other project. It's the single most important tooling habit a Python developer can have — and the most common reason "it worked on my machine" stories happen when someone skips it. This lesson covers venv, activation, pip install, requirements.txt, version pinning, the .gitignore etiquette, and the modern alternatives (pipenv, poetry, uv) you'll meet in some teams.

Why bother — the conflict you avoid

You've worked on Project A with requests==2.28. Project B uses requests==2.32. With one global Python install, both projects share the same requests and you have to pick. Now imagine three projects, ten packages each. Versions clash. Upgrades break old code. CI runs against different versions than your laptop.

A virtual environment fixes this by giving each project its own private site-packages. Project A's requests==2.28 and Project B's requests==2.32 happily coexist on the same machine. Your CI run inside a fresh venv tests against exactly the versions you committed.

Creating a venv

The standard library ships with the venv module. From your project root:

python -m venv venv

The first venv is the module name; the second is the directory it creates. By convention the directory is called venv or .venv. After this, your project folder contains a venv/ directory holding a private Python and an empty site-packages.

Activating a venv

Activation tells your shell to use the venv's python and pip instead of the system ones.

macOS / Linux:

source venv/bin/activate

Windows (PowerShell):

venv\Scripts\Activate.ps1

Windows (CMD):

venv\Scripts\activate.bat

You'll know it worked because your prompt gains a (venv) prefix:

(venv) ~/projects/my-tests $

Now python --version, which python, and pip all point at the venv. Anything you pip install lands in venv/lib/... — only this project sees it.

When you're done, leave the venv with:

deactivate

Installing packages into the venv

With the venv active:

pip install requests
pip install playwright pytest
pip install "requests>=2.30"        # version constraint

A few useful pip commands:

pip list                       # show installed packages
pip show requests              # show one package's metadata
pip uninstall requests         # remove a package
pip install --upgrade requests # upgrade to latest

Always run pip install inside an activated venv. If you accidentally install into the system Python (no venv active), uninstall and start over inside the venv. A fast way to check: which pip (or where pip on Windows) should point to a path under venv/.

requirements.txt — pin and reproduce

To capture exactly what's installed:

pip freeze > requirements.txt

pip freeze outputs every installed package and its exact version, suitable for committing to git:

playwright==1.44.0
pytest==8.2.0
requests==2.32.0
allure-pytest==2.13.5

To reproduce the same environment elsewhere (a teammate's laptop, a CI runner):

python -m venv venv
source venv/bin/activate
pip install -r requirements.txt

The -r flag reads each line as a constraint and installs it. CI scripts almost always end with this incantation.

Version constraints — ==, >=, ~=, *

requests==2.32.0       # exact
requests>=2.30         # at least 2.30, anything newer
requests<2.40          # strictly less than 2.40
requests>=2.30,<3.0    # range
requests~=2.32         # compatible release: ≥2.32, <3.0
requests              # any version (avoid in committed files)

For a test project, pin exact versions (==). Reproducibility matters more than living on the latest minor release — you don't want a surprise requests update breaking CI overnight. For libraries you publish, looser ranges (>=) are more polite to consumers.

~=2.32 (compatible release) is a useful middle ground: accept patch and minor updates, but never a major. Equivalent to >=2.32,<3.0.

A typical QA requirements.txt

The packages a Playwright + pytest project usually pulls in:

playwright==1.44.0
pytest==8.2.0
pytest-playwright==0.5.0
requests==2.32.0
allure-pytest==2.13.5
faker==25.3.0
python-dotenv==1.0.1

Each one earns its keep:

  • playwright / pytest-playwright — the browser automation framework + its pytest adapter.
  • pytest — the test runner (chapter 7).
  • requests — HTTP client for API tests (chapter 4).
  • allure-pytest — reporting (chapter 7).
  • faker — random fake user/address/email data for fixtures.
  • python-dotenv — loads environment variables from .env, common for credentials and base URLs.

Add packages as your project genuinely needs them. Don't seed a project with twenty libraries "just in case."

Two requirements.txt files — runtime vs dev

Many projects split dependencies into two files:

requirements.txt              # things needed to run tests in CI
requirements-dev.txt          # tools used while developing — formatters, linters, type checkers

requirements-dev.txt typically starts with -r requirements.txt so it inherits everything from runtime, then adds tools like black, ruff, mypy. CI installs only requirements.txt; developers install requirements-dev.txt.

.gitignore — never commit venv/

The venv/ directory is huge (a full Python install plus packages), platform-specific (a venv made on macOS won't run on Windows), and reconstructable from requirements.txt. Add it to .gitignore:

venv/
.venv/
__pycache__/
*.pyc
.pytest_cache/

The __pycache__/ and *.pyc lines exclude Python's compiled-bytecode cache. .pytest_cache/ is pytest's cache directory.

If you accidentally committed venv/ once: git rm -r --cached venv/ followed by a commit removes it from the repo while leaving the local files alone.

Modern alternatives

venv + pip + requirements.txt is the lowest common denominator — every Python install supports it. Some teams use higher-level tools that combine venv management, dependency resolution, and lockfiles:

  • pipenv — wraps venv + pip. Stores constraints in Pipfile and exact versions in Pipfile.lock.
  • poetry — full project management: dependencies, building, publishing. Uses pyproject.toml. Popular in libraries and bigger apps.
  • uv — newer, written in Rust, dramatically faster than pip. Drop-in for pip install + pip-compile. Increasingly common in 2026.

For learning Python and most QA projects, plain venv + pip is enough. The skill transfers — once you know how packages and isolation work, switching to a higher-level tool is configuration, not concept.

Activating in CI — no source needed

CI scripts don't usually source venv/bin/activate. Instead they call the venv's executables directly:

# GitHub Actions
- run: python -m venv venv
- run: venv/bin/pip install -r requirements.txt
- run: venv/bin/pytest

venv/bin/pip and venv/bin/pytest already know which Python to use — activation is just a convenience for interactive shells.

Global Python vs venv — the conflict picture

Global Python install vs virtual environment

Global Python — every project shares one

  • One site-packages for the whole machine

  • Project A's requests==2.28 conflicts with Project B's 2.32

  • An OS update can change Python under your projects

  • pip install needs sudo — easy to break the system Python

  • Reproducing a CI environment is guesswork

  • Verdict: use only for tools that are not project-specific

Virtual environment — one per project

  • venv/ folder holds the project's Python and packages

  • Each project pins its own requests version, no conflicts

  • OS-level Python upgrades don't move your project under you

  • pip install never touches the system Python

  • requirements.txt + pip install -r reproduces exactly

  • Verdict: the default for every Python project

Two columns, one obvious choice. The five-second cost of python -m venv venv saves hours of mystery debugging.

A complete setup walkthrough

A new Playwright + pytest project from scratch:

mkdir my-tests && cd my-tests
 
python -m venv venv
source venv/bin/activate            # or venv\Scripts\activate on Windows
 
pip install --upgrade pip           # latest pip first
pip install playwright pytest pytest-playwright requests
 
playwright install chromium         # download the browser binary
 
pip freeze > requirements.txt
 
echo "venv/" > .gitignore
echo "__pycache__/" >> .gitignore
echo ".pytest_cache/" >> .gitignore
 
# write your first test...
echo "def test_smoke(): assert 1 + 1 == 2" > tests/test_smoke.py
 
pytest                              # run it

Eight commands, end to end. Hand the resulting folder to a teammate and they reproduce the whole environment with python -m venv venv && source venv/bin/activate && pip install -r requirements.txt. That's the magic of pinned dependencies plus a venv.

⚠️ Common mistakes

  • Forgetting to activate before pip install. Packages then go into the system Python and stay there. Symptoms: tools work in one terminal but not another, or your script can't import a library you "definitely installed." Fix: deactivate (if needed), reactivate the venv, reinstall.
  • Committing the venv/ folder to git. Adds gigabytes of platform-specific files that don't reproduce on another OS. Fix: add venv/ to .gitignore, then git rm -r --cached venv/ to untrack what you already committed.
  • No version constraints in requirements.txt. requests (no ==) installs the latest version each time, so CI pulls a different version than your laptop. Fix: pip freeze > requirements.txt to capture exact versions, or write requests==2.32.0 by hand.

🎯 Practice task

Set up a fresh QA project. 25-30 minutes.

  1. Create a project folder: mkdir python-qa-playground && cd python-qa-playground.
  2. Create the venv: python -m venv venv.
  3. Activate it. Confirm which python (macOS/Linux) or where python (Windows) shows a path inside venv/.
  4. Upgrade pip: pip install --upgrade pip.
  5. Install three packages: pip install requests pytest python-dotenv.
  6. Run pip list and confirm all three appear.
  7. Create requirements.txt with pip freeze > requirements.txt. Open the file and read the pinned versions.
  8. Add .gitignore containing at least venv/, __pycache__/, .pytest_cache/, and *.pyc.
  9. Deactivate (deactivate). Confirm which python no longer points at the venv. Reactivate.
  10. Try a "fresh-clone" reproduction: deactivate, delete venv/ (rm -rf venv or its Windows equivalent), recreate with python -m venv venv, activate, run pip install -r requirements.txt. Confirm the same versions are installed.
  11. Stretch: add a requirements-dev.txt containing -r requirements.txt and one extra tool — pip install black then pip freeze > requirements-dev.txt (or write the file by hand). Use pip install -r requirements-dev.txt to install both runtime and dev dependencies in one step.

You now have the full toolkit for organising and reproducing a Python project: classes, modules, packages, and isolated environments. The next chapter focuses on the test-writing tool you'll spend the most time with — pytest — and the fixtures, parametrisation, and reporting it provides.

// tip to track lessons you complete and pick up where you left off across devices.