Scenario: Python Development Environment

Chapter 8 — Scenario: Python Development Environment

Installing Python packages directly on Windows can quickly become a mess — conflicting versions between projects, packages that behave differently on Windows vs Linux, and the constant temptation to install things globally and forget about them. A Docker-based development environment solves all of this: every project gets its own clean container with exactly the right Python version and dependencies, isolated from everything else, and guaranteed to behave the same way on any machine.

In this chapter you'll build a FastAPI development environment in Docker, connect VS Code to it so you can edit, run, and debug code as if Python were installed locally — because as far as VS Code is concerned, it is.

The problem with installing Python directly on Windows

Without Docker
• Multiple Python versions conflict (3.10, 3.11, 3.12)
• pip installs are global unless you remember venv
• Package built for Linux doesn't work on Windows
• "It works on my machine" — breaks on the server
• Setting up a second machine means reinstalling everything
• Cleaning up a failed project is painful
With Docker
• Each project has its own Python version, pinned exactly
• Dependencies are inside the container — nothing leaks
• Linux container → same behaviour as your Linux server
• New machine: pull the image, open VS Code, done
• Delete the container → environment is completely gone
• requirements.txt is the only record you need
1
Project Structure

We'll build this in two stages: first a basic development container you can use from the terminal, then the VS Code Dev Containers integration that makes it feel like a native Python install.

fastapi-project/ ├── .devcontainer/ # VS Code Dev Containers config │ └── devcontainer.json ├── app/ │ ├── main.py # FastAPI application │ └── __init__.py ├── tests/ │ └── test_main.py ├── Dockerfile # Development image ├── Dockerfile.prod # Production image (leaner) ├── requirements.txt # Runtime dependencies ├── requirements-dev.txt # Dev-only (pytest, black, etc.) └── .dockerignore
2
The Development Dockerfile

A development Dockerfile is different from a production one — it prioritises iteration speed and developer tools over a small image size.

# Dockerfile (development) FROM python:3.12-slim # Install system tools useful during development RUN apt-get update \ && apt-get install -y --no-install-recommends \ curl git vim \ && rm -rf /var/lib/apt/lists/* WORKDIR /workspace # Install runtime dependencies COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Install dev-only dependencies (testing, linting, formatting) COPY requirements-dev.txt . RUN pip install --no-cache-dir -r requirements-dev.txt # Don't copy source here — we'll bind-mount it for live editing EXPOSE 8000 # Start uvicorn with --reload so it restarts on file changes CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
# requirements.txt — runtime fastapi==0.111.0 uvicorn[standard]==0.30.1 pydantic==2.7.1
# requirements-dev.txt — development only pytest==8.2.2 pytest-asyncio==0.23.7 httpx==0.27.0 # for testing FastAPI routes black==24.4.2 # code formatter ruff==0.4.8 # fast linter mypy==1.10.0 # type checker
# .dockerignore .git __pycache__ *.pyc .env .env.* .venv venv .pytest_cache .mypy_cache .ruff_cache *.egg-info dist/ .devcontainer/
3
Build and Run (Terminal Workflow)

Before setting up VS Code integration, confirm everything works from the terminal. The key technique here is bind-mounting your source code so that uvicorn's --reload picks up every file change instantly.

Terminal — build and run with live reload
# Build the dev image $ docker build -t fastapi-dev:latest . [+] Building 14.2s (8/8) FINISHED # Run with source code bind-mounted # Windows PowerShell — use ${PWD} for the current directory $ docker run -d ` --name fastapi-dev ` -p 8000:8000 ` -v "${PWD}:/workspace" ` fastapi-dev:latest a7c3f9... # Confirm it started and auto-reload is active $ docker logs fastapi-dev INFO: Will watch for changes in these directories: ['/workspace'] INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) INFO: Started reloader process [1] using WatchFiles INFO: Started server process [8] INFO: Application startup complete. # Edit app/main.py on your Windows machine — uvicorn sees the change WARNING: WatchFiles detected changes in 'app/main.py'. Reloading... INFO: Application startup complete.
No rebuild needed when you edit code. Because the source is bind-mounted, changes on your Windows filesystem are immediately visible inside the container. You only rebuild the image when you change requirements.txt (to install new packages).

Running commands inside the dev container

Terminal — one-off commands in the container
# Run the test suite $ docker exec fastapi-dev pytest tests/ -v ========================= test session starts ========================== tests/test_main.py::test_root_returns_200 PASSED [100%] ========================== 1 passed in 0.42s =========================== # Format all code with black $ docker exec fastapi-dev black app/ reformatted app/main.py All done! ✨ 🍰 ✨ # Run the linter $ docker exec fastapi-dev ruff check app/ All checks passed! # Open an interactive shell inside the container $ docker exec -it fastapi-dev bash root@a7c3f9:/workspace# python -c "import fastapi; print(fastapi.__version__)" 0.111.0 root@a7c3f9:/workspace# exit # Install a new package (then add it to requirements.txt) $ docker exec fastapi-dev pip install sqlalchemy # Don't forget: also add sqlalchemy to requirements.txt and rebuild
4
VS Code Dev Containers

The terminal workflow is powerful but you still edit files in Windows while running them in Linux. VS Code Dev Containers goes further: VS Code itself connects inside the container. Your editor, IntelliSense, debugger, linter, and terminal all run in the same Linux environment as the application. There's no mismatch — what VS Code sees is exactly what runs.

🖥️
Windows Host
Displays the VS Code window. Stores your files. Nothing else.
VS Code Server
Runs INSIDE the container. Has Python, linters, debugger.
🐳
Dev Container
Linux · Python 3.12 · All packages · uvicorn --reload

Prerequisites

  • VS Code with the Dev Containers extension installed (ms-vscode-remote.remote-containers)
  • Docker Desktop running
  • That's it — no Python on Windows required

The devcontainer.json file

Create a .devcontainer/devcontainer.json file in your project. This tells VS Code how to build and configure the dev container:

{ "name": "FastAPI Dev", // Use our Dockerfile rather than a pre-built image "build": { "dockerfile": "../Dockerfile", "context": ".." }, // Forward port 8000 from the container to localhost:8000 "forwardPorts": [8000], // VS Code extensions to install inside the container "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.black-formatter", "charliermarsh.ruff", "ms-python.mypy-type-checker", "humao.rest-client" ], "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python", "editor.formatOnSave": true, "editor.defaultFormatter": "ms-python.black-formatter", "terminal.integrated.defaultProfile.linux": "bash" } } }, // Run uvicorn automatically when the container starts "postStartCommand": "uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload &", // Mount the project at /workspace (default for Dev Containers) "workspaceFolder": "/workspace", "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached" }

Opening the project in a Dev Container

Three ways to open:
1. Open VS Code in your project folder → a notification appears bottom-right: "Reopen in Container" — click it.
2. Press F1 → type Dev Containers: Reopen in Container → Enter.
3. Click the blue >< icon in the bottom-left corner of VS Code → Reopen in Container.

VS Code builds the image (first time only, ~30s), starts the container, installs the extensions inside it, and reconnects. The status bar shows Dev Container: FastAPI Dev in blue when you're inside the container. From here, the integrated terminal is a Linux bash shell inside the container — python, pip, pytest all run there.

The first open is slow; every subsequent open is instant. The image is cached — Docker only rebuilds it if you change the Dockerfile or requirements files. When you close VS Code and reopen the folder later, it reconnects to the existing container (or starts a fresh one in seconds if you stopped it).
5
What Works Inside the Dev Container
🔍
IntelliSense & autocomplete
The Python extension runs inside the container and sees all installed packages. Import completions, type hints, and docstrings work for FastAPI, Pydantic, and everything in requirements.txt.
🐛
Full debugger
Set breakpoints in VS Code, press F5, and the debugger attaches to the running FastAPI process inside the container. Step through code, inspect variables, evaluate expressions — as if Python were local.
🧪
Test explorer
The Testing panel discovers and runs pytest tests inside the container. Click the play button next to any test to run it individually, with pass/fail shown inline in the editor.
Format on save
The devcontainer.json enables black formatting on every save. Code is automatically reformatted when you press Ctrl+S — no manual black invocation needed.
🔴
Inline linting
ruff and mypy highlight problems directly in the editor as you type. Unused imports, type mismatches, and style violations show up with squiggly underlines before you even save.
🔗
Port forwarding
Port 8000 is automatically forwarded. Visit http://localhost:8000 or http://localhost:8000/docs in your Windows browser and reach the FastAPI server running inside the container.

Configuring the debugger

Add a .vscode/launch.json to configure the debugger for FastAPI (this file lives on the Windows side, VS Code reads it when connecting to the container):

{ "version": "0.2.0", "configurations": [ { "name": "FastAPI (uvicorn)", "type": "debugpy", "request": "launch", "module": "uvicorn", "args": ["app.main:app", "--host", "0.0.0.0", "--port", "8000"], "jinja": true, "justMyCode": true } ] }
Don't run uvicorn --reload when debugging. The reloader forks a child process that the debugger can't attach to. Stop the auto-reload uvicorn first (Ctrl+C in the terminal), then press F5 to start under the debugger.
6
The Production Dockerfile

The development image has git, vim, pytest, black, and mypy — none of which belong in production. Keep a separate Dockerfile.prod for deploying the application:

# Dockerfile.prod (production — lean and non-root) FROM python:3.12-slim WORKDIR /app # Runtime deps only — no dev tools COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy application source (not the whole project root) COPY app/ ./app/ # Run as non-root RUN useradd -m appuser && chown -R appuser /app USER appuser EXPOSE 8000 # No --reload in production CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
Terminal — build and compare dev vs prod image sizes
$ docker build -t fastapi-dev:latest -f Dockerfile . $ docker build -t fastapi-prod:latest -f Dockerfile.prod . $ docker images fastapi-dev fastapi-prod --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" REPOSITORY TAG SIZE fastapi-dev latest 312MB ← dev tools add ~150MB fastapi-prod latest 161MB ← only what's needed to run
7
Environment Variables and Secrets

FastAPI applications typically read configuration from environment variables (database URLs, secret keys, API keys). Never put these in the Dockerfile — use a .env file for local development and pass them at runtime.

# .env (local development — add to .dockerignore and .gitignore!) DATABASE_URL=sqlite:///./dev.db SECRET_KEY=dev-secret-key-not-for-production DEBUG=true
# Pass the .env file when starting the container docker run -d \ --name fastapi-dev \ -p 8000:8000 \ -v "${PWD}:/workspace" \ --env-file .env \ fastapi-dev:latest # Or pass individual variables with -e docker run -d \ --name fastapi-dev \ -p 8000:8000 \ -v "${PWD}:/workspace" \ -e DATABASE_URL=sqlite:///./dev.db \ -e SECRET_KEY=dev-secret \ fastapi-dev:latest

Read them in FastAPI using Pydantic's BaseSettings:

# app/config.py from pydantic_settings import BaseSettings class Settings(BaseSettings): database_url: str = "sqlite:///./dev.db" secret_key: str debug: bool = False class Config: env_file = ".env" settings = Settings()
Dev Containers and .env files: Add an envFile entry to devcontainer.json to automatically load the .env file when VS Code opens the container:
"runArgs": ["--env-file", "${localWorkspaceFolder}/.env"]
Exercises
  1. Build the dev environment. Create the project structure from Step 1. Write a minimal app/main.py with a single GET / route that returns {"message": "hello"}. Build the dev image and run it with the bind mount. Edit the return message, save the file, and watch uvicorn's reload message appear in docker logs -f fastapi-dev — confirm the change shows at http://localhost:8000 without restarting the container.
  2. Add a route and run the tests. Add a GET /items/{item_id} route that returns {"item_id": item_id}. Write a test in tests/test_main.py using httpx.AsyncClient that calls that route and asserts the response. Run docker exec fastapi-dev pytest tests/ -v and confirm the test passes. Change the route to return a wrong value and watch the test fail.
  3. Set up VS Code Dev Containers. Install the Dev Containers extension, create the .devcontainer/devcontainer.json from Step 4, and use Reopen in Container. Open app/main.py and confirm that typing from fastapi import gives IntelliSense completions for FastAPI classes. Open the integrated terminal and run python --version — confirm it reports Python 3.12 (Linux, not your Windows Python).
  4. Use the debugger. Add the .vscode/launch.json from Step 5. Stop the auto-running uvicorn in the Dev Container terminal (Ctrl+C), then press F5 to start under the debugger. Set a breakpoint on the return line of your / route, visit http://localhost:8000 in your browser, and confirm VS Code pauses at the breakpoint and shows the local variables. Press F5 to continue.
  5. Compare dev and prod image sizes. Build both Dockerfile and Dockerfile.prod. Run docker image history fastapi-prod:latest and confirm there are no pytest, black, ruff, or mypy layers — only the runtime packages. Note the size difference. Consider: which other developer tools might you be tempted to leave in a production image by accident?
Next: Chapter 9 — Scenario: Working with the Claude API
Chapter 9 builds a dedicated Claude API development container: a Python environment pre-configured for working with the Anthropic SDK, with safe .env key handling so your API key never ends up baked into an image or committed to git, plus a simple script structure for running Claude-powered experiments from the terminal or VS Code.