Chapter 0: Development Setup (Optional)
Set up the ideal ML development environment: OS, terminal, VS Code, Python, Git, Jupyter, Docker, and AI coding assistants.
- Topics
- 8
- Videos
- 8
- Min read
- 67
Optional Chapter
This chapter is completely optional for this course. You can skip straight to Python Foundations and start learning right away. That said, most professional AI/ML developers use this exact setup — so if you want to build the same environment the industry uses, this is worth your time.
Chapter Overview
Before writing a single line of ML code, you need a properly configured development environment. This chapter walks you through setting up everything from scratch — even if you've never programmed before.
A good setup is invisible: it stays out of your way and lets you focus on learning. A bad setup creates constant friction — packages that won't install, code that runs differently on different machines, and hours lost debugging environment issues instead of building models.
The ML developer's toolkit:
- Operating System — Ubuntu or macOS (Unix-based for compatibility)
- Terminal — Your command center for everything
- VS Code — The editor where you'll spend most of your time
- Python — The language of ML, properly isolated with virtual environments
- Git & GitHub — Version control for tracking experiments and collaborating
- Jupyter Notebooks — Interactive exploration and visualization
- Docker — Containerized environments for reproducibility (industry standard)
- AI Coding Assistants — Claude Code, Copilot, and Claude extension to accelerate your workflow
Each topic includes installation steps, configuration, and practical tips. By the end of this chapter, you'll have a professional-grade ML development environment ready to go.
Chapter Roadmap
Click any topic to jump in
Operating System
Ubuntu, macOS, or Windows with WSL2 — choosing the right OS and installing system packages for ML.
Command line and editor
Terminal & CLI
Navigation, file operations, pipes, and process management — the command center for all ML work.
VS Code
The editor, extensions for Python and Jupyter, keyboard shortcuts — where you'll write all your code.
Python and Git
Python Installation
Python 3.11+, virtual environments, pip — properly isolated Python that won't break your system.
Git & GitHub
Version control for tracking experiments, collaborating, and never losing work.
Interactive computing and containers
Jupyter Notebooks
Interactive code cells, inline plots, and markdown — the standard for ML exploration and visualization.
Docker
Containers for reproducible environments — run the same code anywhere without dependency conflicts.
AI Coding Assistants
Claude Code, Copilot, and VS Code extensions — AI tools that accelerate your coding workflow.
Topics
8 topics in this chapter
You follow a tutorial for training an image model, and the very first command fails because your machine does not have the tool it calls. Or a package that installs in seconds for everyone else spends twenty minutes trying to compile and then errors out. Most of these problems are not about Python or machine learning at all. They come from the operating system underneath, because the ML ecosystem is built and tested almost entirely on Unix-like systems.
This is the first topic of the Foundations plan, so it assumes nothing beyond a computer you can install software on. Everything later, from the terminal to Docker, runs on top of the choice you make here.
We compare the realistic options. Ubuntu Linux is the default of servers and GPU drivers, macOS is Unix underneath with strong Apple Silicon support, and Windows works well through WSL2 but poorly on its own. Cloud notebooks remove local setup entirely. We finish with three setup steps: installing system packages, configuring a shell profile, and creating SSH keys for GitHub.
Definition
An operating system manages a computer's hardware and runs every other program on top of it. Linux and macOS are Unix-like: they share forward-slash paths, a POSIX shell, and the same core command-line tools. WSL2 runs a real Linux kernel inside a lightweight virtual machine on Windows, giving Windows users the same Linux environment that ML servers and containers use.
In this topic
- 1Ubuntu (recommended)
- 2macOS
- 3Windows + WSL2
- 4Why not plain Windows?
- 5Cloud alternative (optional)
- 6Install essential system packages (Ubuntu/WSL)
- 7Set up your shell profile
- 8Optional: Configure SSH keys for GitHub
Ubuntu (recommended)
Ubuntu is the most widely used OS in ML development. Nearly all cloud instances, Docker containers, and CI/CD pipelines run Ubuntu, so learning it means your local setup matches production. NVIDIA's CUDA toolkit and GPU drivers receive first-class support on Ubuntu, with the latest versions always available through official repos. If you're starting fresh, Ubuntu 22.04 LTS is the safest choice — it has the widest package compatibility and 5 years of security updates.
# Check your Ubuntu version lsb_release -a # Check kernel version uname -r # Check system architecture arch
Distributor ID: Ubuntu Description: Ubuntu 22.04.4 LTS Release: 22.04 Codename: jammy 6.5.0-44-generic x86_64
macOS
macOS is Unix-based, which means nearly all Linux terminal commands work out of the box — no compatibility layers needed. Apple Silicon chips (M1/M2/M3/M4) deliver strong ML performance through Apple's Metal framework and the MLX library optimized for on-device training. The developer experience is polished: Homebrew makes installing packages painless, and tools like iTerm2 and Raycast boost productivity. The main limitation is that NVIDIA CUDA is not available on macOS, so if your workflow depends heavily on CUDA-specific libraries, you'll need a Linux machine or cloud GPU for those tasks.
# Check macOS version sw_vers # Check if running Apple Silicon or Intel uname -m # arm64 = Apple Silicon, x86_64 = Intel # Install Homebrew (macOS package manager) /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Verify Homebrew brew --version
ProductName: macOS ProductVersion: 14.4 BuildVersion: 23E214 arm64 Homebrew 4.2.10
Windows + WSL2
WSL2 (Windows Subsystem for Linux) runs a real Linux kernel inside Windows, giving you full Ubuntu access without dual-booting. Run wsl --install in an admin PowerShell and reboot — you'll have a working Ubuntu terminal in minutes. File system performance is best when you keep your project files inside the Linux filesystem (~/projects/) rather than in /mnt/c/. VS Code's Remote-WSL extension connects seamlessly, so you edit in Windows but run code in Linux.
# Run in PowerShell as Administrator: wsl --install # After reboot, set Ubuntu as default wsl --set-default Ubuntu # Check WSL version wsl --list --verbose # Enter WSL from PowerShell wsl # Inside WSL, verify you're running Linux uname -a
NAME STATE VERSION * Ubuntu Running 2 Linux DESKTOP-ABC123 5.15.153.1-microsoft-standard-WSL2 x86_64 GNU/Linux
Why not plain Windows?
Plain Windows uses backslash path separators (C:\Users\...) while virtually all ML code and documentation assumes forward slashes (/home/user/...). Many Python packages with C extensions fail to compile on Windows because they expect Unix build tools like make and gcc. Shell scripts, Dockerfiles, and CI configurations are written for bash, not PowerShell or CMD. Even when packages do install, subtle differences in file handling, line endings, and environment variables cause hard-to-debug issues that waste hours.
# Demonstrate the path separator difference # Linux/macOS (forward slashes): echo $HOME # /home/user # Windows CMD (backslashes): # echo %USERPROFILE% # C:\Users\user # Check if common Unix tools exist which make gcc bash # On Linux/macOS these exist; on plain Windows they don't # This is why ML tools break on Windows without WSL
/home/user /usr/bin/make /usr/bin/gcc /usr/bin/bash
Cloud alternative (optional)
Google Colab gives you free GPU access (T4) through a Jupyter-like interface in your browser — no local setup required, just a Google account. For more control, AWS EC2 and Lambda Labs offer GPU instances you can SSH into and configure exactly like a local machine. Cloud GPUs are essential if you need to train large models but don't have a local NVIDIA GPU. For this course, a cloud GPU is not necessary — all exercises run fine on CPU — but knowing these options exist helps when you're ready to scale up.
# Check if you have a GPU locally nvidia-smi 2>/dev/null && echo "GPU available" || echo "No NVIDIA GPU found" # SSH into a cloud GPU instance (example) # ssh user@your-cloud-instance # Check GPU on remote machine # nvidia-smi # Google Colab — no install needed: # 1. Go to colab.research.google.com # 2. Runtime → Change runtime type → T4 GPU # 3. Run: !nvidia-smi
No NVIDIA GPU found
Install essential system packages (Ubuntu/WSL)
Many Python packages ship parts written in C or C++. When no pre-built wheel matches your system, pip compiles them, and that fails without a compiler. apt update refreshes the package list, and build-essential supplies gcc, g++ and make, the toolchain Ubuntu (recommended) needs for those builds. python3-venv and python3-dev provide virtual environments and the C headers used by extensions. libgl1-mesa-glx and libglib2.0-0 are shared libraries that OpenCV loads at import time. On Ubuntu 24.04 the first is named libgl1 instead, so apt reports it missing. Node.js is needed later for the Claude Code CLI. The version checks at the end confirm each install.
# Update package list and upgrade existing packages sudo apt update && sudo apt upgrade -y # Install essential build tools (needed to compile Python packages) sudo apt install -y build-essential curl wget git # Install Python prerequisites sudo apt install -y python3 python3-pip python3-venv python3-dev # Install libraries needed by OpenCV, Pillow, and other ML packages sudo apt install -y libssl-dev libffi-dev libgl1-mesa-glx libglib2.0-0 # Install Node.js (needed for Claude Code CLI and Jupyter extensions) curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt install -y nodejs # Verify installations python3 --version git --version node --version
Python 3.12.3 git version 2.43.0 v20.11.0
Set up your shell profile
Every new terminal reads a startup file, ~/.bashrc for bash on Ubuntu and WSL or ~/.zshrc for zsh on macOS, and runs it before showing the prompt. Anything you put there, such as aliases and environment variables, is available in every session. An alias is a short name for a longer command, so activate expands to the command that enables a project's virtual environment and gs to git status. Edits apply only to new terminals until you run source ~/.bashrc to reload the file. Running the lines with >> twice appends duplicate aliases, and writing to .bashrc while using zsh does nothing, because zsh never reads that file.
# Add useful aliases to your shell profile # (Use ~/.zshrc on macOS with zsh, ~/.bashrc on Ubuntu/WSL) echo '' >> ~/.bashrc echo '# ML Development Aliases' >> ~/.bashrc echo 'alias py="python3"' >> ~/.bashrc echo 'alias activate="source .venv/bin/activate"' >> ~/.bashrc echo 'alias jl="jupyter lab"' >> ~/.bashrc echo 'alias gs="git status"' >> ~/.bashrc echo 'alias gc="git commit"' >> ~/.bashrc echo 'alias gp="git push"' >> ~/.bashrc # Reload your shell profile source ~/.bashrc # Test an alias gs # Same as "git status"
Optional: Configure SSH keys for GitHub
Pushing to GitHub over HTTPS means authenticating with a personal access token, which gets tedious. SSH keys replace that with public-key cryptography. ssh-keygen -t ed25519 creates a key pair: the private key stays in ~/.ssh/id_ed25519 and must never be shared, while the .pub file is the public half you paste into GitHub's SSH key settings. The agent holds the unlocked key so you type its passphrase once per session. ssh -T [email protected] should greet you by username. A permission error usually means GitHub has the wrong public key, or your repository remote still uses an HTTPS URL instead of the [email protected]: form.
# Generate an SSH key (press Enter for defaults) ssh-keygen -t ed25519 -C "[email protected]" # Start the SSH agent eval "$(ssh-agent -s)" # Add your key to the agent ssh-add ~/.ssh/id_ed25519 # Copy the public key to clipboard cat ~/.ssh/id_ed25519.pub # Copy the output, then: # Go to GitHub → Settings → SSH Keys → New SSH Key → Paste # Test the connection ssh -T [email protected]
Hi username! You've successfully authenticated, but GitHub does not provide shell access.
A training run on a remote GPU server has no windows, no file browser, and no mouse. There is only a text prompt. The same is true inside Docker containers, on cloud instances, and in the CI systems that test your code. If you can only work through graphical tools, you are locked out of the places where most ML work actually runs. You are also stuck repeating by hand tasks that a single command could do in a second.
The previous topic, Operating System, gave you a Unix-like environment through Ubuntu, macOS, or WSL2. The terminal is how you talk to it directly, and its commands are the same on all three.
We start with navigation, the commands that tell you where you are and move you around. Then come file operations for creating, copying, and deleting, commands for viewing files and logs, and search tools for finding text and files. Piping and redirection chain small commands together, and three hands-on steps build, fill, and query a real project directory.
Definition
A terminal is a text interface to a shell, a program such as bash or zsh that reads commands, runs them, and prints their output. Every command runs in a current working directory and has three standard streams: input, output, and error. Redirection sends a stream to or from a file, and a pipe connects one command's output directly to the next command's input.
In this topic
- 1Navigation
- 2File operations
- 3Viewing files
- 4Searching
- 5Piping & redirection
- 6Create the project directory tree
- 7Populate files and search through them
- 8Use piping, redirection, and process management
Navigation
cd changes your working directory, ls lists files and folders, pwd prints where you currently are, and ~ is a shortcut for your home directory. These four commands account for the majority of terminal usage in day-to-day development. Use cd - to jump back to the previous directory and ls -la to see hidden files (like .gitignore and .env) along with permissions and file sizes. Getting fast at navigation means you spend less time finding things and more time building.
# Print current directory pwd # List files with details (including hidden) ls -la # Change to home directory cd ~ # Create a test directory and navigate into it mkdir -p ~/test-navigation cd ~/test-navigation pwd # Go back to previous directory cd -
/home/user /home/user/test-navigation /home/user
File operations
mkdir -p creates nested directories in one command (e.g., mkdir -p data/raw data/processed). cp -r copies entire folders recursively, mv both moves and renames files, and rm -rf deletes directories and their contents without confirmation — use it carefully. touch creates empty files, which is useful for scaffolding project structures quickly. These commands are the foundation for organizing ML projects: creating data directories, moving model checkpoints, and cleaning up experiment outputs.
# Create nested directories in one command
mkdir -p project/{data/raw,data/processed,models,logs}
# Create empty files
touch project/train.py project/evaluate.py
# Copy a file
cp project/train.py project/train_backup.py
# Rename (move) a file
mv project/train_backup.py project/train_v2.py
# View what we created
ls -R project/
# Remove a file and a directory
rm project/train_v2.py
rm -rf project/logsproject/: data evaluate.py models train.py project/data: processed raw project/data/processed: project/data/raw: project/models:
Viewing files
cat dumps an entire file to your terminal, which works for short files but floods your screen for long ones. head -20 file.txt shows the first 20 lines and tail -20 file.txt shows the last 20 — perfect for checking CSV headers or the end of a training log. less opens a scrollable viewer where you can search with /pattern and quit with q. For ML work, you'll frequently use tail -f logs/training.log to watch training progress update in real time.
# Create a sample file to practice with seq 1 100 > numbers.txt # View entire file (fine for short files) cat numbers.txt | head -5 # First 10 lines head -10 numbers.txt # Last 5 lines tail -5 numbers.txt # Scrollable viewer (press q to quit) less numbers.txt # Count lines, words, characters wc numbers.txt # Watch a file update in real time (Ctrl+C to stop) # tail -f training.log
1 2 3 4 5 96 97 98 99 100 100 100 292 numbers.txt
Searching
grep finds text patterns inside files — grep -rn 'learning_rate' src/ searches recursively and shows line numbers, which is invaluable for finding where hyperparameters are set in a large codebase. find locates files by name or attributes: find . -name '*.py' -mtime -1 finds all Python files modified in the last day. which python tells you the exact path of the Python binary that will run when you type python, which is critical for debugging virtual environment issues.
# Search for a pattern in files recursively grep -rn "import torch" src/ 2>/dev/null || echo "(no matches in current dir)" # Find all Python files modified in the last day find . -name '*.py' -mtime -1 2>/dev/null # Find which Python binary is active which python3 # Search command history for previous commands history | grep "git" | tail -5 # Find large files (useful for cleaning up datasets) find . -size +100M -type f 2>/dev/null || echo "No files over 100MB"
/usr/bin/python3 No files over 100MB
Piping & redirection
The pipe operator | sends the output of one command as input to another, letting you chain simple tools into powerful workflows. For example, cat train_log.txt | grep 'epoch' | tail -5 extracts the last 5 epoch results from a training log. > writes output to a file (overwriting it), while >> appends without erasing existing content. These operators are the backbone of shell scripting — once you're comfortable composing commands with pipes, you can process logs, filter datasets, and automate reporting without writing Python scripts.
# Pipe: chain commands together echo -e "cherry\napple\nbanana" | sort # Write output to a file (overwrite) echo "first line" > output.txt # Append to a file echo "second line" >> output.txt cat output.txt # Count Python files using pipe find . -name '*.py' 2>/dev/null | wc -l # Filter and transform: list files, find .txt, show count ls -1 | grep '\.txt' | wc -l # Redirect errors to /dev/null (suppress error messages) find /etc -name '*.conf' 2>/dev/null | head -3
apple banana cherry first line second line
Create the project directory tree
A consistent layout lets you, your collaborators, and your tools find things without asking. This step builds one in a few commands. mkdir -p creates a directory together with any missing parents and does not complain if it already exists, so the command is safe to rerun. Brace expansion, as in src/{data,models,utils}, is done by the shell before mkdir runs, so one line creates three folders. touch creates empty files. An empty __init__.py marks a folder as a Python package so from src.data import dataset works. Brace expansion needs no spaces after the commas, or the shell treats the braces literally.
# Create the project root
mkdir -p ~/ml-projects/cifar-classifier
cd ~/ml-projects/cifar-classifier
# Create subdirectories in one command
mkdir -p src/{data,models,utils} notebooks configs data/{raw,processed} outputs/{checkpoints,logs}
# Create starter files
touch src/__init__.py src/train.py src/evaluate.py src/predict.py
touch src/data/__init__.py src/data/dataset.py src/data/transforms.py
touch src/models/__init__.py src/models/resnet.py src/models/vgg.py
touch src/utils/__init__.py src/utils/metrics.py src/utils/visualization.py
touch configs/default.yaml README.md requirements.txt .gitignore
# View the structure (install tree if needed: sudo apt install tree)
tree -I __pycache__.
├── configs
│ └── default.yaml
├── data
│ ├── processed
│ └── raw
├── notebooks
├── outputs
│ ├── checkpoints
│ └── logs
├── README.md
├── requirements.txt
├── .gitignore
└── src
├── __init__.py
├── data
│ ├── __init__.py
│ ├── dataset.py
│ └── transforms.py
├── evaluate.py
├── models
│ ├── __init__.py
│ ├── resnet.py
│ └── vgg.py
├── predict.py
├── train.py
└── utils
├── __init__.py
├── metrics.py
└── visualization.pyPopulate files and search through them
Once the tree exists, you fill it with content and practice finding things in it, the two tasks behind most terminal work. A heredoc, cat > file << 'EOF', writes every line up to the closing EOF into the file in one command, and quoting 'EOF' stops the shell from expanding variables inside the text. find . -name "*.py" lists files by pattern. Piping that list into wc -l counts them, and grep searches inside files, as in the Searching concept above. A single > overwrites the target file, so writing .gitignore twice with > keeps only the second version.
# Write a .gitignore cat > .gitignore << 'EOF' .venv/ __pycache__/ *.pyc data/raw/ data/processed/ outputs/ *.egg-info/ .DS_Store wandb/ EOF # Write starter requirements cat > requirements.txt << 'EOF' torch>=2.1.0 torchvision>=0.16.0 numpy>=1.24.0 matplotlib>=3.7.0 pyyaml>=6.0 tqdm>=4.65.0 EOF # Find all Python files find . -name "*.py" | sort # Count how many Python files we created find . -name "*.py" | wc -l # Search for "torch" in requirements grep "torch" requirements.txt # List files sorted by size ls -lhS src/*.py
./src/__init__.py ./src/data/__init__.py ./src/data/dataset.py ./src/data/transforms.py ./src/evaluate.py ./src/models/__init__.py ./src/models/resnet.py ./src/models/vgg.py ./src/predict.py ./src/train.py ./src/utils/__init__.py ./src/utils/metrics.py ./src/utils/visualization.py 13 torch>=2.1.0 torchvision>=0.16.0
Use piping, redirection, and process management
The final step combines everything. A pipe feeds one command's output into the next, so listing, filtering, and counting Python files becomes one line. A single > saves output to a file, replacing its contents, while a double >> appends to it, which is how you keep a growing log. du -sh reports how much disk each folder uses, which helps when datasets or checkpoints fill a drive. export sets an environment variable that programs started from this shell inherit. A trailing ampersand starts a command in the background, jobs lists background tasks, and fg brings one back. Background jobs still stop when the terminal closes.
# Pipe: list files, filter Python, count ls -R | grep "\.py" | wc -l # Redirect output to a file find . -name "*.py" > python_files.txt cat python_files.txt # Append to a file (>> vs >) echo "# Found $(wc -l < python_files.txt) Python files" >> README.md head -1 README.md # Watch disk usage of the project du -sh */ # Environment variables export PROJECT_NAME="cifar-classifier" echo "Working on: $PROJECT_NAME" # Check what's using port 8888 (Jupyter's default) # lsof -i :8888 # Run a long command in the background with & # python src/train.py & # Use 'jobs' to see running background tasks # Use 'fg' to bring it back to foreground
Writing code in a plain text editor means you find a typo only when the script crashes, you switch windows to run anything, and you rename a variable by hunting through files by hand. For ML work it is worse, because you also need notebooks, a terminal, Git, and often a remote GPU machine, each with its own tool. An editor that brings all of these into one window, and understands Python well enough to catch mistakes as you type, removes a great deal of friction.
The previous topic, Terminal & Command Line, gave you the shell. VS Code embeds that same shell in an integrated terminal, and its code command opens projects from it.
We cover installing VS Code and its command-line launcher, and the extensions that add Python language support, notebooks, and Git history. Next come the settings that stop lost work and point the editor at your virtual environment, and the keyboard shortcuts worth learning first. Remote development lets you edit files on a GPU server as if they were local, followed by three setup steps.
Definition
Visual Studio Code is a free, cross-platform source-code editor from Microsoft that is extended through plugins. Language features such as completion, go-to-definition, and error checking come from language servers that the editor talks to over the Language Server Protocol. Settings are stored as JSON, per user or per workspace, and Remote-SSH runs a server-side component on a remote machine while the interface stays local.
In this topic
- 1Installation
- 2Essential extensions
- 3Settings
- 4Keyboard shortcuts
- 5Remote development (optional)
- 6Install VS Code and essential extensions
- 7Configure VS Code settings for ML
- 8Optional: Connect to a remote GPU server via SSH
Installation
Download VS Code from code.visualstudio.com — always use the official site to get the latest stable release. On Ubuntu, download the .deb file and install with sudo dpkg -i code_*.deb, which also sets up the code terminal command automatically. On macOS, drag the .app to Applications, then open VS Code and run "Shell Command: Install 'code' command in PATH" from the command palette. After installation, typing code . in any terminal opens that folder as a VS Code workspace.
# Ubuntu/WSL: Download and install the .deb package wget -qO code.deb 'https://code.visualstudio.com/sha/download?build=stable&os=linux-deb-x64' sudo dpkg -i code.deb rm code.deb # macOS (with Homebrew): # brew install --cask visual-studio-code # Verify the 'code' command works code --version # Open current directory in VS Code code .
1.87.2 e1e3b2a9c2ab8893e15b8c30a2e2ec8b64a77e60 x64
Essential extensions
The Python extension (Microsoft) provides IntelliSense, debugging, and virtual environment detection. Pylance adds fast type checking and auto-imports that catch errors before you run your code. The Jupyter extension lets you run .ipynb notebooks directly in VS Code without a separate server. GitLens annotates each line with its last commit, making it easy to track when and why code changed. Docker and Remote-SSH extensions are optional but become essential when you start deploying models or working on remote GPU servers.
# Install the must-have extensions from the terminal code --install-extension ms-python.python # Python IntelliSense code --install-extension ms-python.vscode-pylance # Fast type checking code --install-extension ms-toolsai.jupyter # Jupyter notebooks code --install-extension eamodio.gitlens # Git blame & history # List your installed extensions code --list-extensions
eamodio.gitlens ms-python.python ms-python.vscode-pylance ms-toolsai.jupyter
Settings
VS Code's defaults suit general editing, and three changes remove most of the friction for Python ML work. files.autoSave set to afterDelay saves about a second after you stop typing, so a script you run from the Terminal & Command Line never uses stale code. editor.formatOnSave runs a formatter such as Black or Ruff on every save, so style stays consistent without effort. python.defaultInterpreterPath set to .venv/bin/python makes linting, completion, and the Run button use your project's virtual environment. If that path is wrong, imports that work in the terminal show red squiggles in the editor. Workspace settings override user settings for one project.
# Open VS Code settings from terminal # Ctrl+Shift+P → "Preferences: Open User Settings (JSON)" # Or view the settings file directly cat ~/.config/Code/User/settings.json 2>/dev/null || echo "No settings file yet" # Key settings to add (via Command Palette or settings.json): # files.autoSave: "afterDelay" → never lose work # editor.formatOnSave: true → auto-format on save # python.defaultInterpreterPath: ".venv/bin/python"
Keyboard shortcuts
Ctrl+P (Quick Open) is the single most useful shortcut — it lets you jump to any file by typing part of its name, far faster than clicking through folders. Ctrl+Shift+P opens the Command Palette where you can search for any VS Code action (change theme, restart Python, format document). Ctrl+\`` toggles the integrated terminal so you can run scripts without leaving the editor. Ctrl+D` selects the next occurrence of your highlighted text for multi-cursor editing, which is perfect for renaming variables across a function.
# View all keyboard shortcuts # Inside VS Code: Ctrl+K Ctrl+S # Most useful shortcuts to memorize: # Ctrl+P → Quick open any file by name # Ctrl+Shift+P → Command palette (search any action) # Ctrl+` → Toggle integrated terminal # Ctrl+D → Select next occurrence (multi-cursor) # Ctrl+Shift+F → Search across all files # Ctrl+B → Toggle sidebar # Ctrl+/ → Toggle comment on selected lines # List your custom keybindings cat ~/.config/Code/User/keybindings.json 2>/dev/null || echo "Using default keybindings"
Using default keybindings
Remote development (optional)
The Remote-SSH extension connects VS Code to a remote machine over SSH, so you edit files locally with full IntelliSense and extensions while code executes on the remote GPU server. This is a game-changer for ML — you get the comfort of your local editor with the compute power of a cloud instance. Set up your SSH config with a named host (Host gpu-server) for one-click connections. Skip this if you're working locally; it becomes relevant when you need to train on machines with GPUs you don't have on your laptop.
# Install the Remote-SSH extension code --install-extension ms-vscode-remote.remote-ssh # Check your SSH config cat ~/.ssh/config 2>/dev/null || echo "No SSH config yet" # Add a host for quick connection (example) # cat >> ~/.ssh/config << 'EOF' # Host gpu-server # HostName 192.168.1.100 # User your-username # IdentityFile ~/.ssh/id_ed25519 # EOF # In VS Code: Ctrl+Shift+P → "Remote-SSH: Connect to Host"
Install VS Code and essential extensions
Install the editor from the official site, then add extensions from the terminal instead of clicking through the marketplace. code --install-extension <id> installs one extension by its identifier, such as ms-python.python, which makes the setup scriptable and repeatable on a new machine. The seven extensions cover Python language support and Pylance type checking, notebooks, Git history, Remote-SSH, Docker, and an AI assistant. The final command lists what is installed so you can confirm each one. If code is not found, the launcher is not on your PATH. On macOS, run the Shell Command action from the Command Palette described in Installation.
# --- Install VS Code --- # Download from https://code.visualstudio.com # Pick the installer for your OS (.deb, .dmg, or .exe) # Install it normally (double-click the downloaded file) # --- Install essential extensions from terminal --- # Python language support + IntelliSense code --install-extension ms-python.python code --install-extension ms-python.vscode-pylance # Jupyter notebooks inside VS Code code --install-extension ms-toolsai.jupyter # Git history and blame annotations code --install-extension eamodio.gitlens # Remote development via SSH (for GPU servers) code --install-extension ms-vscode-remote.remote-ssh # Docker support code --install-extension ms-azuretools.vscode-docker # AI assistants code --install-extension github.copilot # List installed extensions to verify code --list-extensions | grep -E "python|jupyter|gitlens|remote|docker|copilot"
eamodio.gitlens github.copilot ms-azuretools.vscode-docker ms-python.python ms-python.vscode-pylance ms-toolsai.jupyter ms-vscode-remote.remote-ssh
Configure VS Code settings for ML
This step writes the settings described above into settings.json in one command. That file is the source of truth behind the graphical Settings screen, so anything you set in the screen appears there as JSON. Auto-save, format-on-save, and the virtual-environment interpreter are the important lines. The rest set fonts, word wrap, and the color theme. The path shown, ~/.config/Code/User/settings.json, is the Linux location. macOS keeps the file under ~/Library/Application Support/Code/User/. Writing it with a single > replaces any existing settings, so merge by hand if you already have some. A stray trailing comma makes VS Code report the file as invalid.
# Open VS Code settings (JSON)
# Press Ctrl+Shift+P → "Preferences: Open User Settings (JSON)"
# Or edit directly:
cat > ~/.config/Code/User/settings.json << 'SETTINGS'
{
"files.autoSave": "afterDelay",
"files.autoSaveDelay": 1000,
"editor.fontSize": 14,
"editor.fontFamily": "'JetBrains Mono', 'Fira Code', monospace",
"editor.formatOnSave": true,
"editor.minimap.enabled": false,
"editor.wordWrap": "on",
"editor.bracketPairColorization.enabled": true,
"terminal.integrated.fontSize": 13,
"python.defaultInterpreterPath": ".venv/bin/python",
"[python]": {
"editor.defaultFormatter": "ms-python.python",
"editor.tabSize": 4
},
"jupyter.askForKernelRestart": false,
"workbench.colorTheme": "Default Dark Modern"
}
SETTINGS
echo "Settings configured!"Optional: Connect to a remote GPU server via SSH
When the GPU lives on a server, editing over SSH with a terminal editor is slow and error-prone. Remote-SSH fixes that. The step first adds a named host to ~/.ssh/config, so ssh gpu-server expands to the right address, username, and key, and running nvidia-smi there confirms both the login and the GPU. VS Code then connects to that host, installs a small server on it, and opens a window where files, terminals, extensions, and notebooks all run remotely while the interface stays on your laptop. A connection that hangs usually means the SSH login itself fails, so fix plain ssh gpu-server before debugging VS Code.
# --- On your local machine ---
# Add a host to your SSH config for easy access
cat >> ~/.ssh/config << 'EOF'
Host gpu-server
HostName 192.168.1.100
User your-username
IdentityFile ~/.ssh/id_ed25519
ForwardAgent yes
EOF
# Test the SSH connection
ssh gpu-server "nvidia-smi"
# --- In VS Code ---
# Press Ctrl+Shift+P → "Remote-SSH: Connect to Host..."
# Select "gpu-server" from the list
# VS Code opens a new window connected to the remote machine
# Install Python extension on the remote (it will prompt you)
# Now you can:
# - Edit remote files as if they were local
# - Run code on the remote GPU
# - Use the integrated terminal (it's a remote shell)
# - Open Jupyter notebooks on the remote serverYou install a package for one project and another project stops working, because both shared a single Python and the new package upgraded a library the old one depended on. Or you run sudo pip install and break a system tool that the operating system itself relies on. Dependency conflicts like these are among the most common and most confusing problems new ML developers hit. They are avoidable if every project gets its own isolated Python.
The previous topic, VS Code, set up the editor that will run your code. Now you install the Python it runs, in a way that keeps projects separate and lets VS Code find the right interpreter.
We start with choosing a Python version that the major ML libraries publish pre-built packages for. Then we compare two ways of isolating projects: venv, which ships with Python, and conda, which also manages non-Python libraries such as CUDA. After that we cover pip for installing packages and requirements.txt for pinning their versions, and finish with three steps that build and verify a working environment.
Definition
A virtual environment is a self-contained directory holding its own Python interpreter link, its own pip, and its own site-packages folder for installed libraries. Activating it puts its bin directory first on your PATH, so python and pip refer to the project's copy. Packages installed there cannot affect the system Python or any other project.
In this topic
- 1Python 3.10-3.12
- 2venv (recommended for beginners)
- 3conda/miniconda
- 4pip
- 5requirements.txt
- 6Install Python and create the virtual environment
- 7Install ML packages and pin versions
- 8Create .gitignore and verify the setup
Python 3.10-3.12
Python 3.10 through 3.12 are the versions best supported by the ML ecosystem as of 2025-2026. PyTorch, TensorFlow, JAX, and most scientific libraries publish pre-built wheels for these versions, meaning pip install just works without compiling from source. Python 3.13+ introduces breaking changes to the C API that many packages haven't adapted to yet, so you'll hit installation errors with popular libraries. When in doubt, use Python 3.11 — it has the best balance of performance improvements and package compatibility.
# Check your current Python version python3 --version # See where Python is installed which python3 # Check all available Python versions on your system ls /usr/bin/python3* # Install a specific version on Ubuntu sudo apt install -y python3.11 python3.11 --version
Python 3.12.3 /usr/bin/python3 /usr/bin/python3 /usr/bin/python3.12 Python 3.11.9
venv (recommended for beginners)
python -m venv .venv creates a self-contained Python environment inside your project folder. When you activate it with source .venv/bin/activate, python and pip point to this local copy instead of the system Python, so installing packages for one project can't break another. The .venv folder is typically 50-200MB and should be added to .gitignore — never commit it. venv is built into Python (no extra installation), lightweight, and sufficient for most ML projects unless you need to manage multiple Python versions or non-Python binary dependencies.
# Create a virtual environment in your project mkdir -p ~/my-project && cd ~/my-project python3 -m venv .venv # Activate it (prompt changes to show venv name) source .venv/bin/activate # Verify python and pip point to the venv which python which pip # Install a package (goes into .venv only) pip install numpy # Deactivate when done deactivate # After deactivating, python points back to system which python3
/home/user/my-project/.venv/bin/python /home/user/my-project/.venv/bin/pip /usr/bin/python3
conda/miniconda
Conda is both a package manager and an environment manager that handles Python versions and non-Python dependencies like CUDA, cuDNN, and MKL in a single tool. Use Miniconda (the minimal installer) rather than full Anaconda to avoid bloating your system with packages you won't use. Conda environments live in a central location (~/miniconda3/envs/) rather than inside each project, and you create them with conda create -n myenv python=3.11. The main advantage over venv is managing GPU-related libraries — conda install pytorch pytorch-cuda=12.1 -c pytorch -c nvidia handles CUDA setup that would be painful with pip alone.
# Download and install Miniconda (not full Anaconda) wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh bash Miniconda3-latest-Linux-x86_64.sh -b # Initialize conda for your shell ~/miniconda3/bin/conda init bash source ~/.bashrc # Create an environment with a specific Python version conda create -n ml-env python=3.11 -y conda activate ml-env # Install packages (conda handles CUDA automatically) conda install pytorch pytorch-cuda=12.1 -c pytorch -c nvidia # List environments conda env list
# conda environments: base * /home/user/miniconda3 ml-env /home/user/miniconda3/envs/ml-env
pip
pip is Python's default package installer and the tool you'll use most often to add libraries to your projects. Always run pip inside an activated virtual environment — sudo pip install installs packages system-wide, which can break OS-level Python tools and create version conflicts between projects. Use pip install package==1.2.3 to pin a specific version, or pip install package>=1.2,<2.0 for a version range. Run pip install --upgrade pip periodically, as older pip versions sometimes fail to resolve complex dependency chains that newer versions handle correctly.
# Upgrade pip itself (do this periodically) pip install --upgrade pip # Install a specific version pip install numpy==1.26.4 # Install with version range pip install "scikit-learn>=1.4,<2.0" # See what's installed pip list | head -10 # Show details about a package pip show numpy # Uninstall a package pip uninstall numpy -y
Name: numpy Version: 1.26.4 Summary: Fundamental package for array computing in Python Home-page: https://numpy.org Location: /home/user/.venv/lib/python3.12/site-packages
requirements.txt
A requirements.txt file lists every package your project depends on with pinned version numbers, making your environment reproducible. Run pip freeze > requirements.txt to capture your current environment, and pip install -r requirements.txt to recreate it exactly. Pin exact versions (torch==2.1.0) for production and shared projects — without pinning, a teammate running pip install torch might get a different version with different behavior. For cleaner files, maintain a hand-written requirements.txt with just your direct dependencies, and use pip freeze only for full lockfiles.
# Capture current environment to requirements.txt pip freeze > requirements.txt head -5 requirements.txt # Recreate the environment from a requirements file pip install -r requirements.txt # Hand-written requirements.txt (cleaner for projects) cat > requirements.txt << 'EOF' torch>=2.1.0 numpy>=1.24.0 pandas>=2.0.0 matplotlib>=3.7.0 scikit-learn>=1.4.0 EOF # Install from the hand-written file pip install -r requirements.txt
certifi==2024.2.2 charset-normalizer==3.3.2 contourpy==1.2.0 cycler==0.12.1 filelock==3.13.1
Install Python and create the virtual environment
This step creates the project folder and its isolated environment. apt installs the system Python along with the venv module, which Ubuntu packages separately. python3 -m venv .venv creates a .venv directory holding a link to the interpreter, a fresh pip, and an empty site-packages. source .venv/bin/activate puts .venv/bin first on your PATH, and the prompt changes to show the environment's name. The two which commands should print paths inside .venv. If they print /usr/bin, activation failed. Recent Ubuntu releases refuse pip install outside a virtual environment, because PEP 668 marks the system Python as externally managed.
# Install Python (Ubuntu/WSL) sudo apt install -y python3 python3-pip python3-venv # Verify the version python3 --version # Create a project and its virtual environment mkdir -p ~/ml-projects/image-classifier cd ~/ml-projects/image-classifier python3 -m venv .venv # Activate the environment — your prompt will change source .venv/bin/activate # Verify you're using the venv Python (not the system one) which python which pip
Python 3.12.3 /home/user/ml-projects/image-classifier/.venv/bin/python /home/user/ml-projects/image-classifier/.venv/bin/pip
Install ML packages and pin versions
With the environment active, this step installs the core stack and records exactly what was installed. Upgrading pip first avoids resolver bugs in the older copy that venv bundles. NumPy, pandas, and matplotlib handle data and plots, scikit-learn and PyTorch provide models, and JupyterLab with ipykernel adds notebooks. Registering the kernel lets Jupyter find this environment. pip freeze writes every installed package, including indirect dependencies, with an exact == version, so pip install -r requirements.txt rebuilds the same environment elsewhere. Freezing captures platform-specific CUDA builds of PyTorch, so a file frozen on a GPU Linux machine may not install on a Mac.
# Make sure pip is up to date pip install --upgrade pip # Install core scientific Python stack pip install numpy pandas matplotlib seaborn # Install ML frameworks pip install scikit-learn torch torchvision # Install Jupyter for notebooks pip install jupyterlab ipykernel # Register this venv as a Jupyter kernel python -m ipykernel install --user --name=image-classifier # Save exact package versions for reproducibility pip freeze > requirements.txt # View the first 15 packages head -15 requirements.txt # Check total number of installed packages pip list | wc -l
certifi==2024.2.2 charset-normalizer==3.3.2 contourpy==1.2.0 cycler==0.12.1 filelock==3.13.1 fonttools==4.49.0 idna==3.6 ipykernel==6.29.3 jedi==0.19.1 Jinja2==3.1.3 kiwisolver==1.4.5 matplotlib==3.8.3 numpy==1.26.4 packaging==24.0 pandas==2.2.1
Create .gitignore and verify the setup
Two safeguards finish the environment. The .gitignore lists paths Git should never track: the virtual environment, which is large and rebuildable from requirements.txt, Python caches, datasets and model weights too big for a repository, notebook checkpoints, and editor or OS clutter. It must exist before your first git add, because files that are already committed stay tracked even after you ignore them. The smoke test imports NumPy, pandas, PyTorch, and scikit-learn and prints their versions. If any import fails, the environment is incomplete or VS Code is pointing at a different interpreter, and it is far cheaper to find that out now.
# Create a comprehensive .gitignore
cat > .gitignore << 'EOF'
# Virtual environment
.venv/
# Python cache
__pycache__/
*.pyc
*.pyo
# Data and model files (too large for Git)
data/
models/*.pt
models/*.pth
*.h5
# Jupyter checkpoints
.ipynb_checkpoints/
# OS files
.DS_Store
Thumbs.db
# IDE
.vscode/
.idea/
EOF
# Quick smoke test — verify all key imports work
python -c "
import numpy as np
import pandas as pd
import torch
import sklearn
import matplotlib
print(f'NumPy: {np.__version__}')
print(f'Pandas: {pd.__version__}')
print(f'PyTorch: {torch.__version__}')
print(f'Scikit-learn: {sklearn.__version__}')
print(f'Matplotlib: {matplotlib.__version__}')
print(f'CUDA: {torch.cuda.is_available()}')
print()
print('All imports successful!')
"NumPy: 1.26.4 Pandas: 2.2.1 PyTorch: 2.2.1 Scikit-learn: 1.4.1 Matplotlib: 3.8.3 CUDA: False All imports successful!
Last week your model reached 91% accuracy. Since then you have changed the data augmentation, the learning rate, and the architecture, and now it scores 86%. Which change hurt, and what exactly did the code look like when it scored 91%? Without a record of every version, you cannot answer, and saving copies named train_final_v2_really_final.py does not scale or help anyone else working on the same code.
The previous topic, Python Installation, produced a project folder, a pinned requirements.txt, and a .gitignore. Git now tracks that folder's history, and the .gitignore keeps the virtual environment and datasets out of it.
We start with the repository, the folder Git tracks, and the commit, a saved snapshot with a message. Branches let you try an experiment without disturbing working code. The stage, commit, and push cycle moves changes from your editor into history and then onto GitHub, and pull requests put a review step before a merge. Three hands-on steps then build a repository, branch it for experiments, and publish it.
Definition
Git is a distributed version control system that records a project's history as a chain of commits. Each commit is an immutable snapshot of the tracked files, identified by a hash and linked to its parent commits. A branch is a movable pointer to a commit, and a remote such as GitHub is another copy of the repository that you synchronize with through push and pull.
In this topic
- 1Repository (repo)
- 2Commit
- 3Branch
- 4Stage → Commit → Push
- 5Pull requests (PRs)
- 6Initialize a repository and make your first commits
- 7Create branches for ML experiments
- 8Push to GitHub and collaborate
Repository (repo)
A repository is a project folder that Git tracks — it contains your code plus a hidden .git directory storing the entire change history. Create one with git init in an existing folder, or git clone URL to download a remote repo from GitHub. Every ML project should be a Git repo from day one, even personal experiments. The .git folder is what makes a directory a repo; deleting it removes all version history, so never touch it unless you know what you're doing.
# Initialize a new Git repository mkdir -p ~/my-ml-project && cd ~/my-ml-project git init # See the hidden .git directory (this IS the repo) ls -la | grep .git # Or clone an existing repo from GitHub # git clone https://github.com/pytorch/examples.git # Check the repo status git status
Initialized empty Git repository in /home/user/my-ml-project/.git/ drwxrwxr-x 7 user user 4096 .git On branch main No commits yet nothing to commit
Commit
A commit is a snapshot of your project at a specific point in time, tagged with a message, author, and timestamp. Good commit messages describe WHY you made the change, not just what changed — Increase batch size to 64, fixes OOM on 8GB GPU is far more useful than update config. Each commit has a unique hash (like a3b2c1d) that you can use to jump back to that exact state of your code. In ML, commits are especially valuable for reproducing results — you can always check out the exact code that produced a particular model.
# Create a file and commit it echo "# My ML Project" > README.md git add README.md git commit -m "Initial commit: add README" # View the commit log git log --oneline # See details of the last commit git show --stat HEAD # Each commit has a unique hash git rev-parse HEAD
[main (root-commit) a1b2c3d] Initial commit: add README 1 file changed, 1 insertion(+) a1b2c3d Initial commit: add README
Branch
A branch is an independent line of development that diverges from the main codebase. Create one with git checkout -b experiment/new-loss-function to try something risky without affecting working code. If the experiment succeeds, merge the branch back into main; if it fails, the branch stays isolated and main is untouched. In ML workflows, branches are perfect for testing different architectures, hyperparameters, or data preprocessing approaches — each experiment gets its own branch with a descriptive name.
# Create and switch to a new branch git checkout -b experiment/new-model # See all branches (* = current) git branch # Switch back to main git checkout main # Create a branch without switching to it git branch feature/data-pipeline # Delete a branch (after merging) # git branch -d experiment/new-model # List branches with last commit info git branch -v
main * experiment/new-model experiment/new-model a1b2c3d Initial commit: add README feature/data-pipeline a1b2c3d Initial commit: add README * main a1b2c3d Initial commit: add README
Stage → Commit → Push
The Git workflow has three steps: git add stages specific files (marking them for inclusion in the next commit), git commit -m 'message' saves those staged changes as a snapshot in your local repo, and git push uploads commits to a remote like GitHub. Staging exists so you can commit only the files you intend — after adding a new model and fixing a typo, you might commit them separately for a cleaner history. Use git status frequently to see which files are staged, modified, or untracked before committing.
# Check what files have changed git status # Stage specific files git add train.py utils.py # See what's staged vs unstaged git diff --staged # shows what WILL be committed git diff # shows what WON'T be committed yet # Commit the staged changes git commit -m "Add training script and utilities" # View the clean log git log --oneline -5
Pull requests (PRs)
A pull request is a GitHub feature that lets you propose merging changes from one branch into another, giving teammates a chance to review your code before it's merged. You push your branch to GitHub, open a PR through the web interface or gh pr create, and reviewers can comment on specific lines, request changes, or approve. PRs are the standard collaboration workflow in industry — even solo developers use them to maintain a clean history and document decisions. Automated checks (tests, linting) can run on every PR to catch bugs before they reach the main branch.
# Push your branch to GitHub git push origin experiment/new-model # Create a PR using GitHub CLI (install: sudo apt install gh) gh pr create --title "Add ResNet50 model" --body "Implements ResNet50 backbone with 92% accuracy on validation set" # List open PRs gh pr list # View PR details gh pr view 1 # Merge a PR # gh pr merge 1
Initialize a repository and make your first commits
The one-time git config lines attach your name and email to every commit you make. git init turns the project folder into a repository by creating the hidden .git directory, and git status then lists every file as untracked. Staging named paths, rather than everything, is the discipline from Stage → Commit → Push: only source, configs, requirements, .gitignore, and the README go into the first commit. The notebooks go in a second commit with their own message, so the history explains itself. git log --oneline shows both commits, newest first. If git status lists .venv or data files, the .gitignore is missing or misspelled.
# Configure Git identity (one-time setup) git config --global user.name "Your Name" git config --global user.email "[email protected]" # Initialize the repository cd ~/ml-projects/image-classifier git init # See what Git sees — all files are "untracked" git status # Stage specific files (not data or venv!) git add src/ configs/ requirements.txt .gitignore README.md # Create the first commit git commit -m "Initial project setup: directory structure and requirements" # Add and commit the notebooks git add notebooks/ git commit -m "Add exploration notebooks" # View commit history git log --oneline
a3b2c1d Add exploration notebooks f7e8d9c Initial project setup: directory structure and requirements
Create branches for ML experiments
Two model architectures are tried side by side without either touching the working code on main. git checkout -b experiment/resnet50 creates a branch and switches to it. The commit made there, with the learning rate in its message, exists only on that branch. Returning to main and branching again gives the VGG16 experiment its own line of history. Switching back to main shows the edited files in their original state, because checking out a branch rewrites the working folder to match it. git merge brings the winning branch into main. Uncommitted changes follow you when you switch branches, so commit before switching.
# Create and switch to a new branch for an experiment git checkout -b experiment/resnet50 # Make some changes (e.g., edit src/models/resnet.py) echo "# ResNet50 implementation" > src/models/resnet.py git add src/models/resnet.py git commit -m "Implement ResNet50 backbone, lr=0.001" # Try another experiment on a different branch git checkout main git checkout -b experiment/vgg16 echo "# VGG16 implementation" > src/models/vgg.py git add src/models/vgg.py git commit -m "Implement VGG16 backbone for comparison" # List all branches git branch -a # Switch back to main and see it's unchanged git checkout main cat src/models/resnet.py # Empty — changes are on the branch # Merge the winning experiment into main git merge experiment/resnet50 git log --oneline --graph --all
* experiment/vgg16 experiment/resnet50 main * Merge branch 'experiment/resnet50' |\ | * Implement ResNet50 backbone, lr=0.001 * | Implement VGG16 backbone for comparison |/ * Add exploration notebooks * Initial project setup
Push to GitHub and collaborate
Your history so far exists only on your laptop. This step publishes it and covers the everyday commands. git remote add origin records the GitHub repository's address under the name origin, and git push -u origin main uploads main and remembers the pairing, so later pushes need only git push. Experiment branches are pushed by name. Day to day, git pull fetches and merges teammates' work, git diff shows unstaged edits, git diff --staged shows what the next commit will contain, and git blame shows who last changed each line. git checkout -- file throws away uncommitted edits to that file, and nothing can bring them back.
# Create a new repository on GitHub first (via github.com or gh CLI) # Then connect your local repo to GitHub # Using SSH (recommended — set up SSH keys first): git remote add origin [email protected]:yourname/image-classifier.git # Push main branch and set upstream tracking git push -u origin main # Push experiment branches too git push origin experiment/resnet50 git push origin experiment/vgg16 # --- Useful daily workflow commands --- # Pull latest changes from teammates git pull origin main # See what changed since your last commit git diff # See the changes that are staged for commit git diff --staged # Undo changes to a file (before committing) git checkout -- src/train.py # View who changed each line of a file git blame src/train.py
Exploring a new dataset with ordinary scripts is slow. Every time you want to try a different plot or filter, you rerun the whole file, reloading gigabytes of data just to change one line at the bottom. Exploration needs an environment that keeps data in memory between experiments and shows each result, whether a table, a plot, or a number, right next to the code that produced it.
The previous topic, Git & GitHub, gave you version control for scripts. Notebooks are the other half of daily ML work. They also need care in Git, because their files mix code with saved outputs, which makes diffs noisy.
We start with the quickest route, the Jupyter extension in VS Code, which needs no extra installation. Then we look at cells, the units you run one at a time, and the kernel, the Python process that keeps your variables alive between cells. Inline plots show results directly under the code, JupyterLab gives a full browser-based alternative, and three steps set up notebooks and convert them to scripts for production.
Definition
A Jupyter notebook is a JSON document with the .ipynb extension that holds an ordered list of cells, either code or Markdown, together with their saved outputs. Code cells are executed by a kernel, a separate language process that keeps its state between executions. That state therefore depends on the order in which cells were run, not on their order in the file.
In this topic
- 1Quickest start
- 2Cells
- 3Kernel
- 4Inline plots
- 5Advanced: JupyterLab
- 6Quickest start: Jupyter in VS Code
- 7Advanced: Install JupyterLab for a browser-based experience
- 8Optional: Convert notebooks to Python scripts for production
Quickest start
If you already have VS Code, install the Jupyter extension from the Extensions sidebar (Ctrl+Shift+X, search 'Jupyter') and you're done. Create any file ending in .ipynb, and VS Code automatically opens it as a notebook with code cells, a kernel selector, and inline output. No terminal commands, no pip installs, no server management needed. VS Code even picks up your project's virtual environment as the kernel, so all your installed packages are available immediately.
# Install Jupyter extension in VS Code (that's all you need!) code --install-extension ms-toolsai.jupyter # Create a notebook file touch exploration.ipynb # Open it in VS Code code exploration.ipynb # VS Code detects .ipynb and opens the notebook editor # Click "+ Code" to add cells, Shift+Enter to run them # Select your Python interpreter in the top-right kernel picker
Cells
Notebooks are composed of cells — small blocks that you run independently. Code cells execute Python and display output (text, tables, plots) directly below them. Markdown cells contain formatted text with headings, bold, links, and LaTeX equations for documentation. Press Shift+Enter to run the current cell and move to the next one, or Ctrl+Enter to run it and stay in place. The cell-based workflow is what makes notebooks powerful for ML: load your data once in one cell, then experiment with different transformations in subsequent cells without reloading.
# Inside a notebook, you write code in cells
# Here's what the workflow looks like from the terminal:
# Create a quick notebook with nbformat
pip install nbformat -q
python3 -c "
import nbformat
nb = nbformat.v4.new_notebook()
nb.cells = [
nbformat.v4.new_markdown_cell('# My First Notebook'),
nbformat.v4.new_code_cell('import numpy as np\nprint(np.arange(5))'),
nbformat.v4.new_code_cell('# Shift+Enter to run a cell\n# Ctrl+Enter to run and stay'),
]
nbformat.write(nb, 'demo.ipynb')
print('Created demo.ipynb')
"Created demo.ipynb
Kernel
The kernel is the Python process running behind your notebook — it holds all your variables, imported modules, and loaded data in memory. When you run a cell, it sends the code to the kernel for execution and displays the result. If your notebook gets into a weird state (variables you don't expect, import errors after editing modules), restart the kernel from the Kernel menu to get a clean slate. Each notebook has its own kernel, and you can choose which Python environment it uses — make sure it points to your project's virtual environment, not the system Python.
# List available Jupyter kernels jupyter kernelspec list 2>/dev/null || echo "Install jupyterlab to manage kernels" # Register your venv as a kernel pip install ipykernel -q python3 -m ipykernel install --user --name my-project --display-name "My Project (Python 3.12)" # Verify it was added jupyter kernelspec list # Remove a kernel you no longer need # jupyter kernelspec uninstall my-project
Installed kernelspec my-project in /home/user/.local/share/jupyter/kernels/my-project Available kernels: my-project /home/user/.local/share/jupyter/kernels/my-project python3 /usr/share/jupyter/kernels/python3
Inline plots
Notebooks render matplotlib, seaborn, and plotly charts directly below the code cell that generates them — no separate window, no saving to file. In modern Jupyter and VS Code, plots appear inline by default, but you can explicitly use plt.show() or the %matplotlib inline magic command if they don't. This inline visualization is what makes notebooks ideal for data exploration: plot a distribution, see it immediately, adjust parameters, rerun. For interactive plots with zoom and hover, use %matplotlib widget or plotly, which render as fully interactive HTML elements.
# Verify matplotlib is installed for inline plots
python3 -c "
import matplotlib
print(f'matplotlib version: {matplotlib.__version__}')
print(f'backend: {matplotlib.get_backend()}')
print('Inline plots will work in VS Code and JupyterLab')
"
# In a notebook cell, plots appear below the code:
# import matplotlib.pyplot as plt
# plt.plot([1, 2, 3], [1, 4, 9])
# plt.title('My Plot')
# plt.show() ← appears inlinematplotlib version: 3.8.3 backend: agg Inline plots will work in VS Code and JupyterLab
Advanced: JupyterLab
JupyterLab is a full-featured browser-based IDE built around notebooks, with file browsing, multiple tabs, terminals, and extensions. Install it with pip install jupyterlab and launch with jupyter lab — it opens at localhost:8888 in your browser. The key extra step is registering your virtual environment as a kernel with python -m ipykernel install --user --name myenv, otherwise JupyterLab may use the wrong Python. Most beginners should stick with VS Code's built-in Jupyter support; JupyterLab becomes useful when you want a dedicated notebook environment or need its advanced features like drag-and-drop cell rearranging.
# Install JupyterLab pip install jupyterlab # Check the version jupyter lab --version # Register your venv as a kernel first python3 -m ipykernel install --user --name ml-env # Launch JupyterLab (opens browser at localhost:8888) # jupyter lab # Launch on a specific port # jupyter lab --port 8889 # Launch without opening browser (useful for remote) # jupyter lab --no-browser
4.1.5 Installed kernelspec ml-env in /home/user/.local/share/jupyter/kernels/ml-env
Quickest start: Jupyter in VS Code
If you set up VS Code earlier, notebooks need only one extension. code --install-extension ms-toolsai.jupyter adds notebook support, and creating any file ending in .ipynb makes VS Code open it as a notebook instead of as text. Keep notebooks in a notebooks/ folder so they stay separate from the src/ code. When you run the first cell, VS Code asks which kernel to use. Pick the interpreter inside your project's .venv, so the packages from Python Installation are available. If an import fails in the notebook but works in the terminal, the notebook is almost always attached to a different interpreter.
# Install the Jupyter extension (if you haven't already) code --install-extension ms-toolsai.jupyter # Create a notebook file in your project cd ~/ml-projects/image-classifier mkdir -p notebooks touch notebooks/01_exploration.ipynb # Open it in VS Code code notebooks/01_exploration.ipynb # VS Code will automatically: # - Detect it as a Jupyter notebook # - Let you add code/markdown cells # - Use your project's Python interpreter as the kernel # - Show plots inline # That's it! Start writing code in cells and press Shift+Enter to run.
Advanced: Install JupyterLab for a browser-based experience
JupyterLab is the browser-based alternative for people who prefer a dedicated notebook interface. Install it inside the activated virtual environment so it uses that environment's packages. The critical line is python -m ipykernel install --user --name ..., which registers the environment as a named kernel. Without it, JupyterLab may start notebooks on a different Python that lacks your packages. jupyter kernelspec list shows every registered kernel and where it lives. jupyter lab starts a local server, usually on port 8888, and opens it in the browser. Closing the browser tab does not stop that server; press Ctrl+C in the terminal that started it.
# Make sure your venv is activated cd ~/ml-projects/image-classifier source .venv/bin/activate # Install JupyterLab pip install jupyterlab # Register this environment as a Jupyter kernel # (so JupyterLab uses your venv's packages, not the system Python) pip install ipykernel python -m ipykernel install --user --name image-classifier --display-name "Image Classifier (Python 3.12)" # List available kernels jupyter kernelspec list # Launch JupyterLab in your browser jupyter lab # JupyterLab opens at http://localhost:8888 # Create a new notebook → select your kernel → start coding
Available kernels: image-classifier /home/user/.local/share/jupyter/kernels/image-classifier python3 /home/user/.local/share/jupyter/kernels/python3 [I 2024-03-15 10:00:00] JupyterLab application directory is /home/user/.venv/share/jupyter/lab http://localhost:8888/lab?token=abc123...
Optional: Convert notebooks to Python scripts for production
Notebooks are good for exploration but awkward for production. They are hard to test, hard to import, and their mix of code and output makes Git diffs noisy. They also hide state, since cells may have run out of order. jupyter nbconvert --to script writes the notebook's code cells to a .py file, and --no-prompt drops the execution-count markers. Moving the result into src/ turns exploration into a module you can run from the terminal and review in a pull request. Always run the converted script from top to bottom once, because code that depended on a cell run out of order will fail.
# Convert a notebook to a Python script jupyter nbconvert --to script notebooks/01_data_exploration.ipynb # View the generated script cat notebooks/01_data_exploration.py # Clean up the generated script (remove cell markers) # Or use a more targeted approach: jupyter nbconvert --to script --no-prompt notebooks/01_data_exploration.ipynb # Move the clean script to src/ mv notebooks/01_data_exploration.py src/data/explore.py # Pro tip: keep notebooks for exploration, scripts for training # Notebooks → notebooks/ (exploratory, interactive) # Scripts → src/ (production, automated)
Your training code works on your laptop, but on a colleague's machine it crashes with a missing system library, and on the cloud server it runs with a different CUDA version and produces different numbers. Virtual environments from Python Installation isolate Python packages, but they cannot pin the operating system, the system libraries, or the drivers underneath. The famous "it works on my machine" problem lives in exactly that gap.
The previous topic, Jupyter Notebooks, covered interactive exploration. Docker covers the opposite end of the workflow: packaging a finished project so it runs identically anywhere, which is how models are deployed and how training jobs run on shared clusters.
We start with images, the frozen templates that hold an environment, and containers, the running instances made from them. A Dockerfile describes how to build an image layer by layer, Docker Hub supplies ready-made base images, and Docker Compose starts several containers together. Three steps then install Docker, write a Dockerfile for your project, and build and run it.
Definition
Docker is a platform for packaging software as container images. An image is a read-only, layered filesystem snapshot holding an operating system userland, libraries, and application code, built from a Dockerfile. A container is an isolated running instance of an image. It shares the host's kernel, unlike a virtual machine, and gets its own filesystem, process space, and network.
In this topic
- 1Image
- 2Container
- 3Dockerfile
- 4Docker Hub
- 5Docker Compose
- 6Install Docker and verify it works
- 7Write a Dockerfile for your ML project
- 8Build the image and run a container
Image
A Docker image is a read-only template containing an OS, Python, installed packages, and your code — everything needed to run your application. Images are built in layers: each instruction in a Dockerfile (like RUN pip install torch) creates a new layer that gets cached. This means rebuilding after changing only your source code is fast because the pip layer is cached. Images are identified by name and tag (e.g., my-project:v1.0) and can be shared via Docker Hub or private registries so teammates get the exact same environment.
# List all Docker images on your system docker images # Pull a pre-built Python image from Docker Hub docker pull python:3.12-slim # See the image size (slim is ~150MB vs ~1GB for full) docker images python # Pull an ML-ready image with CUDA # docker pull pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime
REPOSITORY TAG IMAGE ID SIZE python 3.12-slim abc123def456 152MB
Container
A container is a running instance of an image — think of the image as a class and the container as an object. Containers are isolated from your host system: they have their own filesystem, network, and process space, so nothing you install or break inside a container affects your laptop. You can run multiple containers from the same image simultaneously, each with different configurations. Containers are ephemeral by default — when they stop, any changes inside them are lost unless you mount volumes for persistent storage.
# Run a Python container interactively
docker run -it --rm python:3.12-slim python3 -c "print('Hello from Docker!')"
# Run a container and get a shell inside it
# docker run -it --rm python:3.12-slim /bin/bash
# List running containers
docker ps
# List all containers (including stopped)
docker ps -a
# Remove stopped containers
docker container prune -fHello from Docker!
Dockerfile
A Dockerfile is a text file with step-by-step instructions for building an image, starting from a base image and adding layers. Order matters for caching: put things that change rarely (system packages, pip install) near the top and things that change often (COPY source code) near the bottom. Use python:3.12-slim as your base instead of python:3.12 to cut image size from 1GB+ to ~150MB. Always include a .dockerignore file to exclude .venv/, data/, and other large directories from the build context, otherwise Docker sends gigabytes of unnecessary data.
# View a minimal Dockerfile for an ML project cat << 'EOF' FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY src/ ./src/ CMD ["python", "src/train.py"] EOF # Always create a .dockerignore to skip large files echo -e ".venv/\ndata/\noutputs/\n__pycache__/\n.git/" > .dockerignore cat .dockerignore
.venv/ data/ outputs/ __pycache__/ .git/
Docker Hub
Docker Hub is a public registry of pre-built images that saves you from installing complex software stacks from scratch. Instead of installing CUDA, cuDNN, and PyTorch manually, you pull pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime and everything is pre-configured and tested. Official images from NVIDIA, PyTorch, and TensorFlow are regularly updated and include GPU support out of the box. You can also push your own images to Docker Hub (or private registries like AWS ECR) to share reproducible environments with teammates or deploy to production.
# Search Docker Hub for ML images docker search pytorch | head -5 # Pull official PyTorch image # docker pull pytorch/pytorch:2.1.0-cuda12.1-cudnn8-runtime # Pull NVIDIA CUDA base image # docker pull nvidia/cuda:12.1.0-base-ubuntu22.04 # Log in to Docker Hub (to push your own images) # docker login # Tag and push an image # docker tag my-project:latest yourusername/my-project:v1.0 # docker push yourusername/my-project:v1.0
NAME DESCRIPTION STARS pytorch/pytorch PyTorch is a deep learning.. 1024 bitnami/pytorch Bitnami container for Py.. 35
Docker Compose
Docker Compose lets you define and run multi-container applications with a single docker-compose.yml file. Instead of remembering long docker run commands with volume mounts and environment variables, you declare everything in YAML and run docker compose up. This is particularly useful for ML serving setups: define a model API container, a database container for predictions, and a monitoring container, all connected on the same network. Compose also supports GPU passthrough, environment files, and health checks, making it the standard tool for local development of multi-service architectures.
# View a sample docker-compose.yml
cat << 'EOF'
version: '3.8'
services:
training:
build: .
volumes:
- ./data:/app/data
- ./outputs:/app/outputs
environment:
- EPOCHS=50
- BATCH_SIZE=32
EOF
# Check Docker Compose version
docker compose version
# Common commands:
# docker compose up → start services
# docker compose up -d → start in background
# docker compose down → stop and remove
# docker compose logs -f → follow logsDocker Compose version v2.24.5
Install Docker and verify it works
This step installs the Docker engine and proves it can run a container. The docker.io package is Ubuntu's build of the engine. On macOS and Windows you install Docker Desktop instead. The engine runs as root, so by default every command needs sudo. usermod -aG docker adds your user to the docker group, which takes effect only after you log out and back in, or run newgrp docker. docker run hello-world downloads a tiny image, starts a container that prints a message, and exits. docker ps lists running containers and docker ps -a includes stopped ones. A permission-denied error on the socket means the group change has not applied.
# Install Docker on Ubuntu/WSL sudo apt install -y docker.io # Add yourself to the docker group (avoids needing sudo) sudo usermod -aG docker $USER # IMPORTANT: Log out and back in for group changes to take effect # Or run: newgrp docker # Verify Docker works docker run hello-world # Check Docker version docker --version # See running containers (should be empty) docker ps # See all containers (including stopped ones) docker ps -a
Hello from Docker! This message shows that your installation appears to be working correctly. Docker version 24.0.7, build afdd53b
Write a Dockerfile for your ML project
The Dockerfile encodes the caching rule from the Dockerfile concept. It starts from python:3.12-slim, a small official base image, and sets /app as the working directory. System packages are installed first, then requirements.txt is copied alone and installed. Because Docker reuses a cached layer until its inputs change, editing your source code rebuilds only the final layers, and the slow pip install is skipped. PYTHONUNBUFFERED makes print output appear immediately in container logs. CMD sets the default command. Copying all the source before installing requirements would invalidate the pip layer on every code change and make each rebuild take minutes.
# Create a Dockerfile in your project root
cd ~/ml-projects/image-classifier
cat > Dockerfile << 'DOCKERFILE'
# Use official Python slim image (smaller than full image)
FROM python:3.12-slim
# Set working directory inside the container
WORKDIR /app
# Install system dependencies first (cached separately from Python deps)
RUN apt-get update && apt-get install -y --no-install-recommends \
git \
&& rm -rf /var/lib/apt/lists/*
# Copy and install Python dependencies (cached if requirements.txt unchanged)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the project code
COPY src/ ./src/
COPY configs/ ./configs/
# Set environment variables
ENV PYTHONUNBUFFERED=1
ENV PYTHONPATH=/app
# Default command
CMD ["python", "src/train.py"]
DOCKERFILE
echo "Dockerfile created!"
cat DockerfileBuild the image and run a container
docker build -t image-classifier:latest . builds the image from the Dockerfile in the current folder and tags it with a name, and docker images shows its size. docker run starts a container from it. Adding -it with a shell gives an interactive session inside, and --rm deletes the container when it exits. The -v flag mounts a host folder into the container, so data is read from your disk and results survive after the container is gone. Without a mount, files written inside disappear with the container. --gpus all needs NVIDIA's container toolkit. The Compose file stores these options in YAML, so docker compose up replaces a long command.
# Build the Docker image (tag it with a name)
docker build -t image-classifier:latest .
# See the built image and its size
docker images | grep image-classifier
# Run the container (training)
docker run --name training-run image-classifier:latest
# Run interactively (get a shell inside the container)
docker run -it --rm image-classifier:latest /bin/bash
# Mount local data directory into the container
docker run -v $(pwd)/data:/app/data image-classifier:latest
# Run with GPU access (requires nvidia-docker)
# docker run --gpus all image-classifier:latest
# Use Docker Compose for complex setups
cat > docker-compose.yml << 'COMPOSE'
version: '3.8'
services:
training:
build: .
volumes:
- ./data:/app/data
- ./outputs:/app/outputs
environment:
- EPOCHS=50
- BATCH_SIZE=32
COMPOSE
# Run with Docker Compose
docker compose upYou know what you want the code to do, such as loading a folder of images, splitting it into training and test sets, and plotting a few samples. But you lose twenty minutes looking up the exact function names and arguments. Or a traceback points into a library you have never read. AI coding assistants address exactly this gap between intent and syntax, and they can explain unfamiliar code as readily as they write new code. They are also confidently wrong often enough that using them well is a skill in itself.
The previous topic, Docker, completed the toolchain for running and shipping code. This final topic of the chapter adds a collaborator inside that toolchain, working in the VS Code editor and the terminal you set up earlier.
We compare three tools in order of depth. The Claude extension adds chat inside VS Code, GitHub Copilot suggests the next lines as you type, and the Claude Code CLI works across a whole project from the terminal. We close with best practices and with using an assistant to learn rather than only to produce code.
Definition
An AI coding assistant is a tool built on a large language model trained on source code and text. It generates, explains, and edits code from natural-language instructions and the surrounding code context. Its output is a statistically likely continuation, not verified logic, so it can be fluent and still wrong. Every suggestion must be read, tested, and understood before it is kept.
In this topic
- 1Claude VS Code Extension (start here)
- 2GitHub Copilot (paid)
- 3Claude Code CLI (advanced, paid)
- 4Best practices
- 5Learning with AI
- 6Install Claude extension in VS Code (start here)
- 7Optional: Install GitHub Copilot for inline suggestions
- 8Advanced: Install Claude Code CLI for terminal-based AI
Claude VS Code Extension (start here)
The Claude extension adds an AI panel to VS Code where you can chat about your code, ask for explanations, and request edits without leaving the editor. It reads the files you have open, so a question like "why is this loss not decreasing?" gets an answer grounded in your actual code rather than a generic one. Installation takes seconds: open Extensions (Ctrl+Shift+X), search for Claude, install Anthropic's extension, and sign in. Using it requires a paid Claude plan or API credits. Proposed edits appear as diffs you accept or reject, which keeps you in control of every change.
# Install the Claude extension from the terminal code --install-extension anthropic.claude-code # Verify it's installed code --list-extensions | grep claude # In VS Code: # 1. Click the Claude icon in the sidebar # 2. Sign in with your Anthropic account # 3. Start chatting: "Explain this function" # 4. Select code → right-click → "Ask Claude"
anthropic.claude-code
GitHub Copilot (paid)
Copilot integrates directly into your editor and predicts what you will type next, showing grey ghost-text suggestions that you accept with Tab. It excels at boilerplate: write a function signature and docstring, and Copilot proposes the body. GitHub offers a limited free tier and paid plans, and verified students can get the paid plan free through GitHub Education. Copilot works best when your code has clear names, comments, and type hints, because the more context it sees about your intent, the better its guesses. Press Ctrl+] to cycle through alternative suggestions. Suggestions are predictions, so they can call functions that do not exist in your installed library version.
# Install Copilot extensions code --install-extension github.copilot code --install-extension github.copilot-chat # Verify installation code --list-extensions | grep copilot # After installing, sign in with your GitHub account # Copilot starts suggesting code as you type # Tab = accept, Esc = dismiss, Ctrl+] = next suggestion
github.copilot github.copilot-chat
Claude Code CLI (advanced, paid)
Claude Code is a terminal-based AI agent that reads your entire project, makes multi-file edits, runs commands, and iterates on errors autonomously. Install with npm install -g @anthropic-ai/claude-code and launch with claude in any project directory. Unlike extension-based tools that only see your current file, Claude Code understands your full codebase — it can refactor data pipelines across dozens of files, write and run tests, and debug complex issues. It requires an Anthropic API key with pay-per-use billing, making it best suited for developers who want maximum productivity on real projects.
# Requires Node.js 18+ node --version # Install Claude Code globally npm install -g @anthropic-ai/claude-code # Verify installation claude --version # Set your API key export ANTHROPIC_API_KEY="sk-ant-your-key-here" # Launch Claude Code in your project # cd ~/ml-projects/my-project # claude
v20.11.0 1.0.0
Best practices
Always read and understand AI-generated code before accepting it — blindly pasting suggestions leads to bugs that are harder to debug because you didn't write the code yourself. Use AI tools for boilerplate, test generation, documentation, and repetitive refactoring where correctness is easy to verify. Write critical logic yourself, especially model architectures, loss functions, and data preprocessing, since these are where subtle bugs have the biggest impact on results. Treat AI suggestions as a starting draft, not a final answer — they occasionally hallucinate API parameters, use deprecated functions, or introduce subtle logic errors.
# Good: use AI for boilerplate and tests
# "Write unit tests for src/models/resnet.py"
# "Add type hints to this function"
# "Generate a docstring for this class"
# Check AI-generated code actually works
python3 -c "
# Always verify AI suggestions by running them
result = sum(range(10))
assert result == 45, f'Expected 45, got {result}'
print('Verification passed!')
print('Always test AI-generated code before committing')
"Verification passed! Always test AI-generated code before committing
Learning with AI
AI assistants are remarkably effective as learning tools — ask Claude to explain a concept step by step, walk through an algorithm with a concrete example, or compare two approaches with pros and cons. When you encounter an error, paste the full traceback and ask for an explanation rather than just a fix — understanding why the error happened builds lasting knowledge. You can ask it to generate progressively harder exercises on any topic, quiz you on concepts, or review your code and suggest improvements. The key habit is to always ask "why" after getting an answer, not just "what" — this turns AI from a crutch into an accelerator for genuine understanding.
# Use AI to learn concepts interactively
# In Claude (VS Code extension or CLI), try:
# "Explain backpropagation step by step with a simple example"
# "Compare Adam vs SGD optimizer — when should I use each?"
# "What does this error mean?" + paste the traceback
# "Give me 5 progressively harder NumPy exercises"
# Example: ask Python to explain itself
python3 -c "
import numpy as np
# Understanding broadcasting — a common source of confusion
a = np.array([[1], [2], [3]]) # shape (3,1)
b = np.array([10, 20, 30]) # shape (3,)
result = a + b # broadcasts to (3,3)
print(f'a shape: {a.shape}')
print(f'b shape: {b.shape}')
print(f'result shape: {result.shape}')
print(result)
"a shape: (3, 1) b shape: (3,) result shape: (3, 3) [[11 21 31] [12 22 32] [13 23 33]]
Install Claude extension in VS Code (start here)
This is the lowest-friction way to add an assistant. code --install-extension anthropic.claude-code installs Anthropic's extension, or you can search for Claude in the Extensions sidebar. After signing in with your Anthropic account, which needs a paid Claude plan or API credits for Claude Code, a sidebar panel opens where you can ask about the file you are editing. You can select code and ask for an explanation, a docstring, or a fix. The assistant proposes edits that you review as a diff before accepting. Treat answers as drafts, and check anything that involves an exact API against the library's documentation.
# Install Claude extension from the terminal code --install-extension anthropic.claude-code # Or install from VS Code: # 1. Open Extensions sidebar (Ctrl+Shift+X) # 2. Search "Claude" → Install Anthropic's official extension # 3. Sign in with your Anthropic account # --- Using Claude in VS Code --- # Open the Claude sidebar (click the Claude icon) # Ask it anything about your code: # "Explain what this function does" # "How do I reshape a NumPy array from (3,4) to (4,3)?" # "Help me fix this TypeError" # "Write a docstring for this class" # You can also select code → right-click → "Ask Claude" # Claude sees your current file and can suggest edits directly
Optional: Install GitHub Copilot for inline suggestions
This step adds Copilot's inline completions next to the chat-based Claude extension. The two extension IDs install the completion engine and Copilot Chat, and you sign in with your GitHub account to activate them. After that, typing a function signature and a docstring is often enough for Copilot to propose the whole body in grey text. The shortcuts in the listing accept, dismiss, and cycle through suggestions. Accepting a suggestion is not the same as checking it. Read it, run it, and keep the docstring precise, because vague comments produce plausible but wrong code. Copilot has a limited free tier, and GitHub Education grants students the paid plan.
# Install Copilot extensions code --install-extension github.copilot code --install-extension github.copilot-chat # After installing: # 1. Click the Copilot icon in the VS Code sidebar # 2. Sign in with your GitHub account # 3. Copilot will start suggesting code as you type # --- Copilot in action --- # When you type a function signature + docstring: # # def train_model(model, train_loader, optimizer, epochs=10): # """Train the model for a given number of epochs.""" # | ← Copilot suggests the entire implementation here # # Press Tab to accept, or Ctrl+] to see alternatives # Copilot Keyboard Shortcuts: # Tab → Accept suggestion # Esc → Dismiss suggestion # Ctrl+] → Next suggestion # Ctrl+[ → Previous suggestion # Ctrl+Enter → Open Copilot completions panel
Advanced: Install Claude Code CLI for terminal-based AI
For power users. Claude Code runs in your terminal with full project access — it can read your entire codebase, make multi-file edits, and run commands. Requires a paid Anthropic API key.
Claude Code CLI is the most powerful option — it sees your entire project and can make complex, multi-file changes. Start with the VS Code extension to learn the basics, then graduate to the CLI when you need more power.
# Install Claude Code CLI (requires Node.js 18+) npm install -g @anthropic-ai/claude-code # Verify installation claude --version # Set your API key (get one from console.anthropic.com) echo 'export ANTHROPIC_API_KEY="sk-ant-..."' >> ~/.bashrc source ~/.bashrc # Navigate to your project and start Claude cd ~/ml-projects/image-classifier claude # Claude Code can now: # - Read and understand your entire codebase # - Make multi-file edits across dozens of files # - Run commands, tests, and debug errors # - "Create a training script for ResNet50 on CIFAR-10" # - "Debug why my model isn't converging" # - "Refactor the data pipeline to support augmentation" # --- Best Practices for all AI tools --- # 1. Always read and understand AI-generated code before using it # 2. Use AI for boilerplate, tests, and documentation # 3. Write critical model architecture and loss functions yourself # 4. Don't paste sensitive data (API keys, credentials) into AI tools