Python Virtual Environments — The Right Way to Manage Dependencies

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

Every professional Python project needs a virtual environment. Without one, all packages install globally, leading to version conflicts between projects, polluted system Python, and "works on my machine" deployment failures. A virtual environment creates an isolated Python installation per project, so each project has its own dependency tree.

In 2026, the Python tooling landscape has expanded significantly. venv + pip remain the baseline, but uv — a blazing-fast Rust-based package manager — has become the go-to choice for speed-conscious teams, and poetry continues to be popular for projects that need lockfiles and publishing. Understanding all of these makes you adaptable across codebases.

Virtual environments are also the first step toward containerization — Docker Python images mirror what virtual environments enforce: a clean, reproducible Python runtime with pinned dependencies.

venv — Built-In Virtual Environment Tool

# Create a virtual environment
python -m venv .venv
 
# Activate on macOS/Linux
source .venv/bin/activate
 
# Activate on Windows
.venv\Scripts\activate
 
# Verify
which python        # should point to .venv/bin/python
python --version    # Python 3.12.x
 
# Deactivate
deactivate

pip — Installing Packages

# Install a package
pip install fastapi
 
# Install specific version
pip install fastapi==0.115.0
 
# Install minimum version
pip install "fastapi>=0.100.0"
 
# Install from requirements file
pip install -r requirements.txt
 
# Upgrade a package
pip install --upgrade fastapi
 
# Uninstall
pip uninstall fastapi
 
# List installed packages
pip list
 
# Show details of a package
pip show fastapi

requirements.txt

# Generate from current environment
pip freeze > requirements.txt
# requirements.txt
fastapi==0.115.0
uvicorn==0.30.0
pydantic==2.7.0
httpx==0.27.0
sqlalchemy==2.0.30
# Install from file
pip install -r requirements.txt

requirements structure for dev dependencies

# requirements.txt — production deps
fastapi==0.115.0
uvicorn==0.30.0
pydantic==2.7.0
 
# requirements-dev.txt — dev deps
-r requirements.txt
pytest==8.2.0
mypy==1.10.0
ruff==0.4.0
black==24.4.0

uv — The Fast Modern Package Manager

uv is a Rust-based Python package manager that is 10-100x faster than pip.

# Install uv
pip install uv
# or: curl -LsSf https://astral.sh/uv/install.sh | sh
 
# Create a new project
uv init myproject
cd myproject
 
# Add dependencies
uv add fastapi uvicorn
uv add --dev pytest mypy ruff
 
# Run a command in the environment
uv run python main.py
uv run pytest
 
# Sync dependencies from lock file
uv sync
 
# Generate lockfile
uv lock

uv creates a pyproject.toml and uv.lock file for reproducible installs.

pyproject.toml — Modern Project Configuration

[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.115.0",
    "uvicorn>=0.30.0",
    "pydantic>=2.7.0",
]
 
[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "mypy>=1.10",
    "ruff>=0.4",
]
 
[tool.ruff]
line-length = 100
target-version = "py311"
 
[tool.mypy]
strict = true
python_version = "3.11"

poetry — Alternative Package Manager

# Install poetry
pip install poetry
 
# Create a new project
poetry new myproject
cd myproject
 
# Add dependency
poetry add fastapi
 
# Add dev dependency
poetry add --dev pytest
 
# Install all dependencies
poetry install
 
# Run in virtual env
poetry run python main.py
poetry run pytest
 
# Export to requirements.txt
poetry export -f requirements.txt --output requirements.txt

.gitignore for Python Projects

# .gitignore
.venv/
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/
.mypy_cache/
.pytest_cache/
.ruff_cache/
*.env
.DS_Store

Environment Variables with python-dotenv

pip install python-dotenv
# .env file (never commit to git!)
DATABASE_URL=postgresql://user:pass@localhost/mydb
API_KEY=your-secret-key
DEBUG=true
# main.py
from dotenv import load_dotenv
import os
 
load_dotenv()  # loads .env into os.environ
 
db_url = os.environ["DATABASE_URL"]
debug = os.getenv("DEBUG", "false").lower() == "true"

Common Mistakes

  • Installing packages globally instead of inside a virtual environment
  • Committing .venv/ to git — it is OS-specific and can be huge
  • Not pinning versions in requirements.txt — causes non-reproducible builds
  • Forgetting to activate the virtual environment before running scripts
  • Having multiple virtual environment directories (.venv, env, venv) and confusing them

Best Practices

  • Always name your virtual environment .venv — it is the standard and gitignore rules cover it
  • Use uv for new projects — it is dramatically faster than pip
  • Pin exact versions in production; use ranges in libraries
  • Store all dev tooling config (mypy, ruff, pytest) in pyproject.toml
  • Never store secrets in code — always use environment variables with python-dotenv

Key Takeaways

  • Virtual environments isolate project dependencies, preventing version conflicts between projects
  • python -m venv .venv creates a virtual environment; source .venv/bin/activate activates it
  • pip freeze > requirements.txt captures the exact installed package versions
  • uv is the fastest Python package manager in 2026 — 10-100x faster than pip
  • pyproject.toml is the modern standard for project configuration, replacing setup.py
  • Never commit .venv/ or .env to git — add them to .gitignore
  • Use separate requirements.txt and requirements-dev.txt to separate production from dev dependencies

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading