Python Environments
Dependency hell is real. Virtual environments are the cure.
Prerequisites: Phase 0, Lesson 01
Video coming soon
An AI-narrated walkthrough for this module is on the way. Everything you need is in the lesson below.
#Learning Objectives
- Create isolated virtual environments using
uv,venv, orconda - Write a
pyproject.tomlwith optional dependency groups and generate lockfiles for reproducibility - Diagnose and fix common pitfalls: global installs, pip/conda mixing, CUDA version mismatches
- Implement a per-phase environment strategy for projects with conflicting dependencies
#The Problem
You install PyTorch 2.4 for a fine-tuning project. Next week, a different project needs PyTorch 2.1 because its CUDA build is pinned. You upgrade globally, and the first project breaks. You downgrade, and the second one breaks.
This is dependency hell. It happens constantly in AI/ML work because:
- PyTorch, JAX, and TensorFlow each ship their own CUDA bindings
- Model libraries pin specific framework versions
- A global
pip installoverwrites whatever was there before - CUDA 11.8 builds don't work with CUDA 12.x drivers (and vice versa)
The fix: every project gets its own isolated environment with its own packages.
#The Concept
graph TD
subgraph without["Without virtual environments"]
SP[System Python] --> T24["torch 2.4.0 (CUDA 12.4)\nProject A needs this"]
SP --> T21["torch 2.1.0 (CUDA 11.8)\nProject B needs this"]
SP --> CONFLICT["CONFLICT: only one\ntorch version can exist"]
end
subgraph with["With virtual environments"]
PA["Project A (.venv/)"] --> PA1["torch 2.4.0 (CUDA 12.4)"]
PA --> PA2["transformers 4.44"]
PB["Project B (.venv/)"] --> PB1["torch 2.1.0 (CUDA 11.8)"]
PB --> PB2["diffusers 0.28"]
end
#Build It
#Option 1: uv venv (Recommended)
uv is the fastest Python package manager (10-100x faster than pip). It handles virtual environments, Python versions, and dependency resolution in one tool.
curl -LsSf https://astral.sh/uv/install.sh | sh
uv python install 3.12
cd your-project
uv venv
source .venv/bin/activateInstall packages:
uv pip install torch numpyCreate a project with pyproject.toml in one step:
uv init my-ai-project
cd my-ai-project
uv add torch numpy matplotlib#Option 2: venv (Built-in)
If you can't install uv, Python ships with venv:
python3 -m venv .venv
source .venv/bin/activate # Linux/macOS
.venv\Scripts\activate # Windows
pip install torch numpySlower than uv, but works everywhere Python is installed.
#Option 3: conda (When You Need It)
Conda manages non-Python dependencies like CUDA toolkits, cuDNN, and C libraries. Use it when:
- You need a specific CUDA toolkit version without installing it system-wide
- You're on a shared cluster where you can't install system packages
- A library's install instructions say "use conda"
# Install miniconda (not the full Anaconda)
curl -LsSf https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -o miniconda.sh
bash miniconda.sh -b
conda create -n myproject python=3.12
conda activate myproject
conda install pytorch torchvision torchaudio pytorch-cuda=12.4 -c pytorch -c nvidiaOne rule: if you use conda for an environment, use conda for all packages in that environment. Mixing pip install into a conda env causes dependency conflicts that are painful to debug.
#For This Course: Per-Phase Strategy
You could create one environment for the whole course. Don't. Different phases need different (sometimes conflicting) dependencies.
Strategy:
ai-engineering-from-scratch/
├── .venv/ <-- shared lightweight env for phases 0-3
├── phases/
│ ├── 04-neural-networks/
│ │ └── .venv/ <-- PyTorch env
│ ├── 05-cnns/
│ │ └── .venv/ <-- same PyTorch env (symlink or shared)
│ ├── 08-transformers/
│ │ └── .venv/ <-- might need different transformer versions
│ └── 11-llm-apis/
│ └── .venv/ <-- API SDKs, no torch neededThe script in code/env_setup.sh creates the base environment for this course.
#pyproject.toml Basics
Every Python project should have a pyproject.toml. It replaces setup.py, setup.cfg, and requirements.txt in one file.
[project]
name = "ai-engineering-from-scratch"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
"numpy>=1.26",
"matplotlib>=3.8",
"jupyter>=1.0",
"scikit-learn>=1.4",
]
[project.optional-dependencies]
torch = ["torch>=2.3", "torchvision>=0.18"]
llm = ["anthropic>=0.39", "openai>=1.50"]Then install:
uv pip install -e ".[torch]" # base + PyTorch
uv pip install -e ".[llm]" # base + LLM SDKs
uv pip install -e ".[torch,llm]" # everything#Lockfiles
A lockfile pins every dependency (including transitive ones) to exact versions. This guarantees reproducibility: anyone who installs from the lockfile gets exactly the same packages.
# uv generates uv.lock automatically when using uv add
uv add numpy
# pip-tools approach
uv pip compile pyproject.toml -o requirements.lock
uv pip install -r requirements.lockCommit your lockfile to git. When someone clones the repo, they install from the lockfile and get identical versions.
#Common Mistakes
#1. Installing globally
pip install torch # BAD: installs to system Python
source .venv/bin/activate
pip install torch # GOOD: installs to virtual environmentCheck where your packages go:
which python # should show .venv/bin/python, not /usr/bin/python
which pip # should show .venv/bin/pip#2. Mixing pip and conda
conda create -n myenv python=3.12
conda activate myenv
conda install pytorch -c pytorch
pip install some-other-package # BAD: can break conda's dependency tracking
conda install some-other-package # GOOD: let conda manage everythingIf you must use pip inside conda (some packages are pip-only), install all conda packages first, then pip packages last.
#3. Forgetting to activate
python train.py # uses system Python, missing packages
source .venv/bin/activate
python train.py # uses project Python, packages foundYour shell prompt should show the environment name:
(.venv) $ python train.py#4. Committing .venv to git
echo ".venv/" >> .gitignoreVirtual environments are 200MB-2GB. They're local, not portable between machines. Commit pyproject.toml and the lockfile instead.
#5. CUDA version mismatch
nvidia-smi # shows driver CUDA version (e.g., 12.4)
python -c "import torch; print(torch.version.cuda)" # shows PyTorch CUDA version
# These must be compatible.
# PyTorch CUDA version must be <= driver CUDA version.#Use It
Run the setup script to create your course environment:
bash phases/00-setup-and-tooling/06-python-environments/code/env_setup.shThis creates a .venv at the repo root with core dependencies installed and verified.
#Exercises
- Run
env_setup.shand verify all checks pass - Create a second virtual environment, install a different version of numpy in it, and confirm the two environments are isolated
- Write a
pyproject.tomlfor a project that needs both PyTorch and the Anthropic SDK - Deliberately install a package globally (without activating a venv), notice where it goes, then uninstall it
#Key Terms
| Term | What people say | What it actually means |
|---|---|---|
| Virtual environment | "A venv" | An isolated directory containing a Python interpreter and packages, separate from the system Python |
| Lockfile | "Pinned dependencies" | A file listing every package and its exact version, guaranteeing identical installs across machines |
| pyproject.toml | "The new setup.py" | The standard Python project configuration file, replacing setup.py/setup.cfg/requirements.txt |
| Transitive dependency | "A dependency of a dependency" | Package B depends on C; if you install A which depends on B, C is a transitive dependency of A |
| CUDA mismatch | "My GPU isn't working" | PyTorch was compiled for a different CUDA version than what your GPU driver supports |
Run this lesson in the Code Lab
A real JupyterLab, with this lesson's code files (and a ready-to-run notebook). The Quick Lab opens instantly in your browser. The Cloud Lab adds a terminal, saved files and the full toolchain.
#!/usr/bin/env bash
set -euo pipefail
PYTHON_MIN_MAJOR=3
PYTHON_MIN_MINOR=11
VENV_DIR=".venv"
CORE_PACKAGES="numpy matplotlib jupyter scikit-learn pandas"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
pass() { echo -e " ${GREEN}[PASS]${NC} $1"; }
fail() { echo -e " ${RED}[FAIL]${NC} $1"; }
warn() { echo -e " ${YELLOW}[WARN]${NC} $1"; }
REPO_ROOT="$(cd "$(dirname "$0")/../../../.." && pwd)"
cd "$REPO_ROOT"
echo ""
echo "=== AI Engineering from Scratch: Python Environment Setup ==="
echo ""
echo "Repo root: $REPO_ROOT"
echo ""
HAS_UV=false
if command -v uv &> /dev/null; then
HAS_UV=true
pass "uv found: $(uv --version)"
else
warn "uv not found. Install it: curl -LsSf https://astral.sh/uv/install.sh | sh"
warn "Falling back to python3 -m venv + pip"
fi
PYTHON_CMD=""
for cmd in python3 python; do
if command -v "$cmd" &> /dev/null; then
version=$("$cmd" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')" 2>/dev/null || true)
if [ -n "$version" ]; then
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
if [ "$major" -ge "$PYTHON_MIN_MAJOR" ] && [ "$minor" -ge "$PYTHON_MIN_MINOR" ]; then
PYTHON_CMD="$cmd"
break
fi
fi
fi
done
if [ -z "$PYTHON_CMD" ]; then
fail "Python ${PYTHON_MIN_MAJOR}.${PYTHON_MIN_MINOR}+ not found"
echo ""
echo "Install Python ${PYTHON_MIN_MAJOR}.${PYTHON_MIN_MINOR}+:"
echo " uv: uv python install 3.12"
echo " macOS: brew install python@3.12"
echo " Linux: sudo apt install python3.12 python3.12-venv"
exit 1
fi
pass "Python: $($PYTHON_CMD --version)"
echo ""
echo "--- Creating virtual environment ---"
echo ""
if [ -d "$VENV_DIR" ]; then
warn "Existing $VENV_DIR found. Reusing it."
else
if $HAS_UV; then
uv venv "$VENV_DIR"
else
"$PYTHON_CMD" -m venv "$VENV_DIR"
fi
pass "Created $VENV_DIR"
fi
if [ -f "$VENV_DIR/bin/activate" ]; then
source "$VENV_DIR/bin/activate"
elif [ -f "$VENV_DIR/Scripts/activate" ]; then
source "$VENV_DIR/Scripts/activate"
else
fail "Could not find activation script in $VENV_DIR"
exit 1
fi
pass "Activated virtual environment"
VENV_PYTHON="$(which python)"
if [[ "$VENV_PYTHON" != *"$VENV_DIR"* ]]; then
fail "Python is not running from the venv: $VENV_PYTHON"
exit 1
fi
pass "Python path: $VENV_PYTHON"
echo ""
echo "--- Installing core packages ---"
echo ""
if $HAS_UV; then
uv pip install $CORE_PACKAGES
else
pip install --upgrade pip
pip install $CORE_PACKAGES
fi
pass "Installed: $CORE_PACKAGES"
echo ""
echo "--- Verifying installation ---"
echo ""
FAILURES=0
verify_package() {
local pkg=$1
local import_name=${2:-$1}
if python -c "import $import_name; print(f' $pkg: {${import_name}.__version__}')" 2>/dev/null; then
return 0
else
fail "$pkg"
FAILURES=$((FAILURES + 1))
return 1
fi
}
verify_package "numpy" "numpy"
verify_package "matplotlib" "matplotlib"
verify_package "scikit-learn" "sklearn"
verify_package "pandas" "pandas"
verify_package "jupyter" "jupyter_core"
echo ""
python -c "
import numpy as np
a = np.random.randn(3, 3)
b = np.random.randn(3, 3)
c = a @ b
print(f' Matrix multiply check: ({a.shape}) @ ({b.shape}) = ({c.shape})')
"
pass "NumPy operations working"
echo ""
if python -c "import torch" 2>/dev/null; then
TORCH_VERSION=$(python -c "import torch; print(torch.__version__)")
CUDA_AVAIL=$(python -c "import torch; print(torch.cuda.is_available())")
pass "PyTorch $TORCH_VERSION (CUDA: $CUDA_AVAIL)"
else
warn "PyTorch not installed (install later when needed):"
echo " uv pip install torch torchvision torchaudio"
fi
echo ""
echo "=== Summary ==="
echo ""
echo " Repo root: $REPO_ROOT"
echo " Venv: $REPO_ROOT/$VENV_DIR"
echo " Python: $(python --version)"
echo " Packages: $CORE_PACKAGES"
echo ""
if [ "$FAILURES" -gt 0 ]; then
fail "$FAILURES package(s) failed verification"
exit 1
else
pass "All checks passed"
echo ""
echo "Activate this environment in future sessions:"
echo ""
echo " source $REPO_ROOT/$VENV_DIR/bin/activate"
echo ""
fi
5 questions on this lesson
Answer each one and see straight away whether you were right and why. Your first answer to a question counts. You are not signed in, so this is practice and nothing is saved.
The text and code of this lesson are based on AI Engineering from Scratch by Rohit Ghumare, MIT licensed.