handbook/docs/environments.md
disqualifier c53d67da2f add uv as a parallel option alongside pip
uv is presented as the recommended faster, standards-compliant drop-in;
pip stays the baseline/fallback. uv command syntax verified against
docs.astral.sh/uv before writing.

environments.md:
- new 'uv (optional, faster)' section: install from pyproject
  (uv pip install . / -e . / '.[dev]'), the uv sync managed-venv flow,
  and a note that uv.lock is local-only/gitignored (not committed)
- pinning subsection: pin direct deps in pyproject via == or git @ref,
  with a warning that this pins direct deps only — transitive deps still
  float at build time (documented tradeoff)
- uv equivalents added beside pip in the Local .venv and Docker tabs

deploy.md:
- 'Faster builds with uv' tip: uv-from-ghcr COPY, ENV UV_COMPILE_BYTECODE=1,
  uv pip install --system . (reads pyproject, no lock)
- layer-caching shows the uv variant beside the pip one
- checklist notes uv pip install + UV_COMPILE_BYTECODE; fix stray 'configs'
  plural -> 'config'

.gitignore: ignore uv.lock (local-only, never committed).

Verified in-browser; mkdocs build --strict clean (anchors resolve).

Signed-off-by: disqualifier <dev@disqualifier.me>
2026-06-30 04:40:10 -04:00

7.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 -e .              # install the project from pyproject.toml
```

Or with [uv](#uv-optional-faster) — same `pyproject.toml`, much faster:

```bash
uv venv                       # create .venv
uv pip install -e .           # install from pyproject.toml
```

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 pyproject.toml .
RUN pip install --no-cache-dir .    # or: uv pip install .
COPY . .
```

This is how things run in production — see the [Deploy guide](deploy.md) for the
full container standard (uid 1337, mounts, layer caching, and the uv image
setup).

!!! 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.

uv (optional, faster)

!!! tip "uv is the recommended fast path; pip stays the baseline" uv is a faster, standards-compliant drop-in for pip. It reads the same pyproject.toml — no workflow change required, and pip keeps working exactly as before. Use it wherever you'd reach for pip; the rest of this handbook shows the pip command with the uv equivalent beside it.

Install deps straight from pyproject.toml (no requirements.txt needed):

uv pip install .            # install the project + its deps
uv pip install -e .         # editable (dev) install
uv pip install '.[dev]'     # with an extras group, e.g. dev

If you'd rather have uv manage the venv for you, use the managed-venv flow:

uv sync                     # create/refresh .venv from pyproject.toml + uv.lock
uv run python -m yourapp    # run inside the managed env, no manual activate

!!! note "uv.lock is local-only — never committed" uv sync writes a uv.lock for your machine's resolved environment. It is gitignored, not committed — we don't ship a lockfile. Pinning happens in pyproject.toml (below), not the lock.

Pinning deps

Pin direct dependencies in pyproject.toml with == or a git @ref:

[project]
dependencies = [
    "requests==2.31.0",
    "mylib @ git+https://git.rethinkstudios.io/rethink-public/mylib.git@<sha>",
]

!!! warning "This pins direct deps only — transitive deps still float" == / @ref pins the packages you list. Their dependencies still resolve fresh at build time. That's an accepted tradeoff — documented on purpose — not an oversight: we pin what we depend on directly and let the rest float.

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-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 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)!
  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 cdonly if you use pyenv-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-version so 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 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 page.