03 — Development Environment

The development environment is where you spend 90% of your working hours. A bad setup creates friction on every action: slow autocomplete, broken debugger, inconsistent formatting, notebooks that pollute git history with 50MB of embedded outputs. This document gives you the setup that eliminates that friction from Day 1.

The core choice in 2025: VS Code with the Jupyter extension beats both standalone JupyterLab and the classic Jupyter Notebook interface for most ML workflows. You get the notebook experience inside a full IDE — with a real debugger, Git integration, terminal, and extension ecosystem.


VS Code Setup for ML

Install VS Code

Download from code.visualstudio.com. On macOS:

# Via Homebrew
brew install --cask visual-studio-code

# Or download the .dmg directly from the site

Essential Extensions

Install these extensions. In VS Code: Cmd+Shift+X → search by name → Install.

Extension

Publisher

Why It’s Essential

Python

Microsoft

Base Python support: syntax highlighting, import resolution, environment selection

Pylance

Microsoft

Fast type checking, autocomplete, IntelliSense. Required.

Jupyter

Microsoft

Run .ipynb notebooks directly in VS Code. Variable explorer, plot viewer.

GitLens

GitKraken

Inline git blame, branch history, diff viewer. Makes git usable in-editor.

Black Formatter

Microsoft

Auto-format Python on save (uses Black).

Ruff

Charliermarsh

Fast Python linter (replaces flake8, isort, many pylint rules). Rust-based, instant.

indent-rainbow

oderwat

Colored indentation levels. More useful than it sounds for reading nested code.

Path Intellisense

Christian Kohler

Autocomplete file paths in strings. Essential for ML data paths.

GitHub Copilot

GitHub

AI autocomplete. If you have access, useful. Not a substitute for understanding.

Optional but useful:

  • Rainbow CSV — Syntax highlighting for CSV files

  • Docker — If you’re containerizing ML workflows

  • Remote - SSH — Connect to cloud VMs directly from VS Code (critical for Vast.ai/RunPod workflows)

  • Markdown All in One — For writing README/documentation

VS Code Settings for ML

Open settings: Cmd+, → switch to JSON view (top-right icon). Add:

{
    "editor.formatOnSave": true,
    "editor.defaultFormatter": "ms-python.black-formatter",
    "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
    "python.analysis.typeCheckingMode": "basic",
    "jupyter.notebookFileRoot": "${workspaceFolder}",
    "jupyter.sendSelectionToInteractiveWindow": true,
    "files.autoSave": "afterDelay",
    "files.autoSaveDelay": 1000,
    "editor.rulers": [88],
    "python.analysis.extraPaths": ["${workspaceFolder}/src"],
    "[python]": {
        "editor.defaultFormatter": "ms-python.black-formatter",
        "editor.codeActionsOnSave": {
            "source.organizeImports": "always"
        }
    }
}

What this does:

  • Formats Python on every save with Black (88-char line length)

  • Sets the project .venv as the default interpreter

  • Shows a ruler at 88 characters (Black’s default)

  • Auto-saves files after 1 second


Jupyter vs JupyterLab vs VS Code Notebooks

An honest comparison:

Feature

Classic Jupyter

JupyterLab

VS Code + Jupyter

Debugger

❌ Limited

⚠️ Basic

✅ Full Python debugger

Variable explorer

⚠️

✅ Built-in

Git integration

⚠️ Extension

✅ Native GitLens

Autocomplete

⚠️ Basic

⚠️ Basic

✅ Pylance (excellent)

Terminal

⚠️ Separate tab

✅ Integrated

✅ Integrated

Extensions

Limited

JupyterLab extensions

VS Code extensions (massive ecosystem)

.py file editing

⚠️

✅ Full IDE

Startup time

Fast

Medium

Medium

Remote access

✅ Browser-based

✅ Browser-based

Via Remote SSH

Verdict: Use VS Code for local development. Use JupyterLab when you need browser-based access to a remote server (Kaggle/Colab have their own interfaces anyway). Never use the classic Jupyter Notebook (jupyter notebook) for new work — it’s the 2012 interface.


Notebook Hygiene: nbstripout

Jupyter notebooks store outputs (plots, print statements, arrays) as JSON inside the .ipynb file. A single notebook with matplotlib plots can balloon to 5MB. Multiply this across a project and you have a multi-hundred-MB git history full of useless binary data.

Solution: nbstripout. It strips outputs from notebooks before every git commit.

# Install
pip install nbstripout

# Enable in your git repo (one-time setup per repo)
nbstripout --install

# Verify it's installed
cat .git/config | grep nbstripout
# Should show: filter = nbstripout

# Optional: add to .gitattributes for the whole team
nbstripout --install --attributes .gitattributes

After this, git add on any .ipynb file will automatically strip outputs. Your notebook files stay small. Your git history stays clean.

What most people get wrong: They commit notebooks with outputs and then wonder why their git repo is 500MB. By the time you notice, the history is already bloated. Install nbstripout on Day 1.


Code Quality: Black + Ruff + isort

These three tools handle formatting, linting, and import sorting. In 2025, Ruff replaces both flake8 and isort (and many pylint rules) with a single, much faster tool. Black handles formatting.

Install

pip install black ruff pre-commit
# or with uv:
uv add --dev black ruff pre-commit

Configure in pyproject.toml

[tool.black]
line-length = 88
target-version = ['py311']
include = '\.pyi?$'

[tool.ruff]
line-length = 88
target-version = "py311"
select = [
    "E",   # pycodestyle errors
    "W",   # pycodestyle warnings
    "F",   # pyflakes
    "I",   # isort
    "N",   # pep8-naming
    "UP",  # pyupgrade
]
ignore = [
    "E501",  # line too long (Black handles this)
]

[tool.ruff.isort]
known-first-party = ["src"]

Pre-commit hooks (run formatters automatically before every commit)

# Create .pre-commit-config.yaml
cat > .pre-commit-config.yaml << 'EOF'
repos:
  - repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
      - id: black
        language_version: python3.11

  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.7
    hooks:
      - id: ruff
        args: [--fix]

  - repo: https://github.com/kynan/nbstripout
    rev: 0.7.1
    hooks:
      - id: nbstripout

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
        args: ['--maxkb=1000']
EOF

# Install the hooks
pre-commit install

# Test: run on all files
pre-commit run --all-files

After this, every git commit will automatically:

  1. Format your Python files with Black

  2. Lint and fix imports with Ruff

  3. Strip notebook outputs with nbstripout

  4. Reject files larger than 1MB


The Productive ML Development Workflow

The correct ML workflow in 2025:

Explore in notebook  →  Refactor to .py  →  Track with W&B  →  Version with Git

Phase 1: Exploration (notebook)

Use notebooks for:

  • Data exploration and visualization

  • Trying out model architectures

  • Debugging outputs at each step

  • Quick experiments that you might throw away

Keep notebooks in a notebooks/ directory. Name them with numbers and descriptions: 01_data_exploration.ipynb, 02_baseline_model.ipynb.

Phase 2: Production (.py files)

Once you know something works, refactor it into Python modules:

src/
  data/
    dataset.py       # Dataset class, data loading
    transforms.py    # Data augmentations
  models/
    transformer.py   # Model architecture
    layers.py        # Custom layers
  training/
    trainer.py       # Training loop
    losses.py        # Loss functions
  utils/
    metrics.py       # Evaluation metrics
    logging.py       # Logging utilities
train.py             # Entry point
evaluate.py          # Evaluation script

Why this matters: A notebook is not a reproducible artifact. A Python script with CLI arguments is. The moment you want to run the same experiment 5 times with different hyperparameters, you need .py files, not notebooks.

Phase 3: Track everything

From the first real training run, use W&B (free tier):

import wandb

wandb.init(
    project="my-ml-project",
    config={
        "learning_rate": 1e-4,
        "batch_size": 32,
        "epochs": 10,
        "architecture": "resnet50",
    }
)

for epoch in range(epochs):
    train_loss = train_one_epoch(model, train_loader, optimizer)
    val_loss, val_acc = evaluate(model, val_loader)
    
    wandb.log({
        "epoch": epoch,
        "train_loss": train_loss,
        "val_loss": val_loss,
        "val_accuracy": val_acc,
    })

wandb.finish()

Keyboard Shortcuts Worth Learning (VS Code)

Shortcut (macOS)

Action

Cmd+Shift+P

Command palette — search any command

Cmd+P

Quick file open

Cmd+/

Toggle comment

Cmd+D

Select next occurrence (multi-cursor)

Cmd+Shift+F

Find in all files

F12

Go to definition

Shift+F12

Find all references

`Ctrl+Shift+``

Open new terminal

Cmd+B

Toggle sidebar

In notebook: Shift+Enter

Run cell, move to next

In notebook: Cmd+Enter

Run cell, stay

In notebook: A / B

Insert cell above / below (command mode)


Return to README.md · Previous: 02_gpu_and_compute_strategy.md · Next: 04_version_control_for_ml.md