Every Python project should have its own virtual environment. It’s not optional — it’s how you avoid dependency hell, reproducibility issues, and the dreaded “but it works on my machine.”

Why Virtual Environments?

Without virtual environments:

  • Project A needs requests==2.25
  • Project B needs requests==2.31
  • Both use system Python
  • One project breaks

With virtual environments:

  • Each project has isolated dependencies
  • Different Python versions per project
  • Reproducible across machines
  • No sudo required for installing packages

The Built-in Way: venv

Python 3.3+ includes venv:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Create environment
python3 -m venv venv

# Activate (Linux/macOS)
source venv/bin/activate

# Activate (Windows)
venv\Scripts\activate

# Your prompt changes
(venv) $ which python
/path/to/project/venv/bin/python

# Install packages
pip install requests flask

# Deactivate
deactivate

Project Structure

myprovstrp.jereeygencsqpicvturttmsioiy/rjgaeenpmcopetr/n.ettso.mtlxt#Virtualenvironment(gitignored)
1
2
3
4
5
# .gitignore
venv/
.venv/
__pycache__/
*.pyc

Managing Dependencies

requirements.txt

1
2
3
4
5
# Freeze current environment
pip freeze > requirements.txt

# Install from requirements
pip install -r requirements.txt

Better: separate dev and production requirements:

#fgp#-pbmlusrylyranyrtapesicerecyqkcoqesk=u=opuqt==i=rgiu==1r3n2ri=2.e.=-er738m0=bme...e.2iem410n01nne.2t.atn3.s2rst1..y-st0=d.x=ett2vx..t(9tp.xr9toduction)

Pin Your Versions

Bad:

frleaqsukests

Good:

frleaqsuke=s=t3s.=0=.20.31.0

Best: Use a lock file (pip-tools, poetry, or pdm).

pip-tools: Better Dependency Management

1
pip install pip-tools

Create requirements.in with direct dependencies:

#frcleeraqlesueqkerusyitrsements.in

Generate locked requirements.txt:

1
pip-compile requirements.in

Output includes all transitive dependencies with pinned versions:

#fwrleerarqeskuqk#z#e#u=esi=vuvtvr3igisie.a=a=am0==e.-3f2-n0r.l.rt0a3sr.s1r.e1k.etq0qxuutiirr(eegmmeeennnettrssa..tiiennd)

Upgrade dependencies:

1
pip-compile --upgrade requirements.in

Sync environment to match lock file:

1
pip-sync requirements.txt

Poetry: Modern Dependency Management

Poetry handles virtual environments, dependencies, and packaging:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Install poetry
curl -sSL https://install.python-poetry.org | python3 -

# Create new project
poetry new myproject

# Or init in existing project
poetry init

# Add dependencies
poetry add flask requests
poetry add --group dev pytest black

# Install dependencies
poetry install

# Run commands in environment
poetry run python app.py
poetry run pytest

# Activate shell
poetry shell

pyproject.toml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[tool.poetry]
name = "myproject"
version = "0.1.0"
description = "My awesome project"
authors = ["Your Name <you@example.com>"]

[tool.poetry.dependencies]
python = "^3.11"
flask = "^3.0.0"
requests = "^2.31.0"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.3"
black = "^23.12.1"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

Poetry creates poetry.lock automatically — commit this file.

pyenv: Multiple Python Versions

System Python shouldn’t be touched. Use pyenv to manage Python versions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Install pyenv (macOS)
brew install pyenv

# Install pyenv (Linux)
curl https://pyenv.run | bash

# Add to shell config
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(pyenv init -)"' >> ~/.bashrc

Usage:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# List available versions
pyenv install --list

# Install specific version
pyenv install 3.12.1

# Set global default
pyenv global 3.12.1

# Set project-specific version
cd myproject
pyenv local 3.11.7
# Creates .python-version file

# List installed versions
pyenv versions

pyenv + venv Together

1
2
3
4
5
6
7
8
9
# Set Python version for project
pyenv local 3.11.7

# Create venv with that version
python -m venv venv

# Verify
source venv/bin/activate
python --version  # 3.11.7

Conda: For Data Science

When you need non-Python dependencies (C libraries, CUDA, etc.):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Create environment
conda create -n myenv python=3.11

# Activate
conda activate myenv

# Install packages
conda install numpy pandas scikit-learn
pip install some-pip-only-package

# Export environment
conda env export > environment.yml

# Recreate environment
conda env create -f environment.yml

environment.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
name: myenv
channels:
  - conda-forge
  - defaults
dependencies:
  - python=3.11
  - numpy=1.26
  - pandas=2.1
  - pip
  - pip:
    - some-pip-package==1.0.0

Docker: The Ultimate Isolation

For complete reproducibility, use Docker:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
FROM python:3.11-slim

WORKDIR /app

# Install dependencies first (better caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["python", "app.py"]

No virtual environment needed inside the container — the container IS the isolation.

Common Patterns

Makefile for Common Tasks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
.PHONY: venv install test lint clean

venv:
	python3 -m venv venv

install: venv
	./venv/bin/pip install -r requirements.txt
	./venv/bin/pip install -r requirements-dev.txt

test:
	./venv/bin/pytest

lint:
	./venv/bin/black --check .
	./venv/bin/mypy .

clean:
	rm -rf venv __pycache__ .pytest_cache

CI/CD Pipeline

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# GitHub Actions
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
          
      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt
          pip install -r requirements-dev.txt
          
      - name: Run tests
        run: pytest

direnv Integration

Automatically activate venv when entering directory:

1
2
3
4
5
# Install direnv
brew install direnv  # or apt install direnv

# Add to shell
echo 'eval "$(direnv hook bash)"' >> ~/.bashrc

Create .envrc:

1
2
3
# .envrc
source venv/bin/activate
export DEBUG=true
1
direnv allow

Now the environment activates automatically when you cd into the project.

Quick Reference

ToolUse Case
venvSimple projects, built-in
pip-toolsBetter dependency locking
poetryModern projects, packaging
pyenvMultiple Python versions
condaData science, non-Python deps
dockerComplete isolation

The Minimal Setup

For most projects, this is enough:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# One-time
python3 -m venv venv
echo "venv/" >> .gitignore

# Every time
source venv/bin/activate
pip install -r requirements.txt

# When adding dependencies
pip install newpackage
pip freeze > requirements.txt

Graduate to poetry or pip-tools when dependency management gets complex.


Virtual environments are one of those things that seem tedious until you’ve debugged a dependency conflict at 2 AM. Spend five minutes setting up properly now, save hours of pain later.