# Virtual environments How we keep a project's Python isolated — from the system Python and from every other project. The rule underneath all of it: **never install into, or upgrade, the system Python.** The OS depends on it; a project must never touch it. !!! danger "Leave the system Python alone" No `sudo pip install`, no upgrading the system interpreter for a project. If a project needs a different version or a package, that goes in a **virtual environment** — never the system one. Breaking system Python can break the OS. ## Project-based isolation Every project runs against its own isolated environment. There are three ways that happens, depending on where the project runs: === "Local `.venv`" A virtualenv living in the repo. Simplest for day-to-day local work. ```bash python -m venv .venv # create it (once) source .venv/bin/activate # activate for this shell pip install -r requirements.txt ``` Keep `.venv/` **gitignored** — it's per-machine, never committed. === "Makefile-driven" Wrap the venv in a `make` target so every dev (and CI) sets up identically — no "did you activate it?" drift. ```makefile VENV := .venv PY := $(VENV)/bin/python $(VENV): requirements.txt python -m venv $(VENV) $(PY) -m pip install -r requirements.txt .PHONY: run run: $(VENV) $(PY) -m yourapp ``` `make run` creates the venv if missing, installs deps, and runs — all against the isolated interpreter, no manual activation. === "Docker" The container **is** the isolation — its own filesystem, its own interpreter, nothing shared with the host. You don't need a `.venv` inside an image; install straight into the container's Python. ```dockerfile FROM python:3.12-slim COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . ``` This is how things run in production — see the [Deploy guide](deploy.md) for the full container standard (uid 1337, mounts, layer caching). !!! tip "Which one?" **Local `.venv`** for quick iteration, **Makefile** when you want repeatable setup across the team, **Docker** for anything that ships. They're not exclusive — a project often has a `.venv` for local dev *and* a Dockerfile for deploy. ## Local dev with pyenv For local work you also need the right **Python version**, not just isolated deps. We target **Python 3.10+**, and [pyenv](https://github.com/pyenv/pyenv) installs and switches versions per-project without touching the system Python. - **Per-project pinning:** a `.python-version` file in a repo makes pyenv auto-select that interpreter when you `cd` in — everyone on the project runs the same one. - **Pairs with venvs:** combined with `pyenv-virtualenv`, each project gets both an isolated version *and* isolated deps. ### Install Follow the [official pyenv installation](https://github.com/pyenv/pyenv#installation) for the installer and build dependencies — no point reproducing it here. Then add the shell init below to your `~/.zshrc` (or `~/.bashrc`) and restart your shell. ### Shell init Without these lines pyenv's shims aren't on `PATH`, so `pyenv` and auto-version-switching won't work: ```bash # pyenv — Python version management export PYENV_ROOT="$HOME/.pyenv" export PATH="$PYENV_ROOT/bin:$PATH" eval "$(pyenv init --path)" # (1)! eval "$(pyenv init -)" # (2)! eval "$(pyenv virtualenv-init -)" # (3)! ``` 1. Puts the **shims** dir on `PATH`, so `python` resolves to the pyenv-selected version instead of the system one. 2. Shell integration — command rehashing and completion. 3. Auto-activates a project's virtualenv on `cd` — **only** if you use `pyenv-virtualenv`. Drop this line if you don't. ### Everyday use ```bash pyenv install 3.10.14 # install a version (one-time) pyenv install --list # see available versions cd pyenv local 3.10.14 # writes .python-version -> auto-selects here python --version # confirms the pinned version ``` - **`pyenv local `** per project — commit the `.python-version` so the team matches. - **`pyenv global `** for your default outside any project. ## Shell quality-of-life extras A couple of optional lines worth having alongside pyenv: ```bash # flake8 with our shared config (max line 120) alias flake8='flake8 --config ~/.config/flake8' # local bins on PATH (pip --user installs, npm globals) export PATH="$HOME/.local/bin:$PATH" export PATH="$HOME/.npm-global/bin:$PATH" ``` - The **flake8 alias** keeps everyone linting with the same config (our 120 max-line, etc. — see [Standards](standards.md#files-and-style)). - The **`.local/bin` / npm-global** PATH lines stop "command not found" after a `pip install --user` or a global npm install. !!! note "More shell setup" This is the Python-env slice. The fuller dev shell setup — the WSL/Windows clipboard bridges, per-repo git-identity aliases, and the `gl` graph log — is on the [Workflow](workflow.md#handy-shell-setup) page.