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\S cripts\a ctivate
# Your prompt changes
( venv) $ which python
/path/to/project/venv/bin/python
# Install packages
pip install requests flask
# Deactivate
deactivate
Project Structure# m ├ ├ │ ├ ├ ├ └ y ─ ─ ─ ─ ─ ─ p ─ ─ ─ ─ ─ ─ r o v s └ t r p . j e r ─ e e y g e n c ─ s q p i c v t u r t t m s i o i y / r j g a e e n p m c o p e t r / n . e t t s o . m t l x t # V i r t u a l e n v i r o n m e n t ( g i t i g n o r e d ) 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:
# f g p # - p b m l u s r y l y r a n y r t a p e s i c e r e c y q k c o q e s k = u = o p u q t = = i = r g i u = = 1 r 3 n 2 r i = 2 . e . = - e r 7 3 8 m 0 = b m e . . . e . 2 i e m 4 1 0 n 0 1 n n e . 2 t . a t n 3 . s 2 r s t 1 . . y - s t 0 = d . x = e t t 2 v x . . t ( 9 t p . x r 9 t o d u c t i o n )
Pin Your Versions# Bad:
f r l e a q s u k e s t s
Good:
f r l e a q s u k e = s = t 3 s . = 0 = . 2 0 . 3 1 . 0
Best: Use a lock file (pip-tools, poetry, or pdm).
Create requirements.in with direct dependencies:
# f r c l e e r a q l e s u e q k e r u s y i t r s e m e n t s . i n
Generate locked requirements.txt:
1
pip-compile requirements.in
Output includes all transitive dependencies with pinned versions:
# f w r l e e r a r q e s k u q k # z # e # u = e s i = v u v t v r 3 i g i s i e . a = a = a m 0 = = e . - 3 f 2 - n 0 r . l . r t 0 a 3 s r . s 1 r . e 1 k . e t q 0 q x u u t i i r r ( e e g m m e e e n n n e t t r s s a . . t i i e n n d )
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
Now the environment activates automatically when you cd into the project.
Quick Reference# Tool Use 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.