New docs/environments.md, nav 'Virtual environments' (before Deploy): - the rule: never touch system Python (danger callout) - project-based isolation as Local .venv / Makefile-driven / Docker tabs, each with a runnable snippet; Docker ties back to the deploy standard - local dev with pyenv: why, official install link, shell init with annotated lines, everyday use, per-project .python-version - shell quality-of-life extras (flake8 alias, .local/bin + npm-global PATH), cross-ref'd to Standards and Workflow workflow.md pyenv bullet now points at the new page; index.md gains a card. Verified in-browser: tabs switch (Local/Makefile/Docker), annotations and admonitions render; mkdocs build --strict clean (cross-ref anchors resolve). Signed-off-by: disqualifier <dev@disqualifier.me>
5.0 KiB
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 installs and switches versions per-project without touching the system Python.
- Per-project pinning: a
.python-versionfile in a repo makes pyenv auto-select that interpreter when youcdin — 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
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:
# 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)!
- Puts the shims dir on
PATH, sopythonresolves to the pyenv-selected version instead of the system one. - Shell integration — command rehashing and completion.
- Auto-activates a project's virtualenv on
cd— only if you usepyenv-virtualenv. Drop this line if you don't.
Everyday use
pyenv install 3.10.14 # install a version (one-time)
pyenv install --list # see available versions
cd <project>
pyenv local 3.10.14 # writes .python-version -> auto-selects here
python --version # confirms the pinned version
pyenv local <ver>per project — commit the.python-versionso the team matches.pyenv global <ver>for your default outside any project.
Shell quality-of-life extras
A couple of optional lines worth having alongside pyenv:
# 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).
- The
.local/bin/ npm-global PATH lines stop "command not found" after apip install --useror 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 page.