codeceibe ran on the usual stack for a long time: pyenv to pin 3.13.x, a per-checkout venv, and a requirements.txt compiled by hand whenever a dependency changed. It worked, but every fresh clone cost ten minutes of "activate the venv, remember which pyenv shim is active, reinstall because someone forgot to pin a transitive dependency." We moved the whole thing to uv over a single afternoon.
Pinning the interpreter
uv python pin 3.13
cat .python-version
# 3.13
uv python pin writes a .python-version file and, if that exact 3.13 build isn't already on the machine, uv downloads a prebuilt standalone interpreter -- no pyenv install, no waiting on a from-source build the first time someone sets up the repo.
From requirements.txt to a lockfile
uv init --app --no-readme
uv add $(cat requirements.txt | grep -v '^#' | cut -d= -f1)
uv add --dev pytest pytest-django ruff
uv lock
This produces a pyproject.toml with real dependency constraints and a uv.lock that pins every transitive package, hash included. uv sync from then on reproduces the exact environment -- no more "works on my machine" from someone whose pip install happened to resolve a different minor version of django-filter.
How much faster it actually is
That's the same requirements.txt, same machine, Docker build cache cleared between runs so it's measuring the actual resolve-and-download work. uv sync against a warm cache and an existing lockfile is close to instant because uv hardlinks packages out of its global cache instead of re-downloading and re-extracting them per project.
The gotchas
- Editable installs need a real
pyproject.toml. Our internalccpostbundle-format package was still a baresetup.pywith no build backend declared.uv pip install -e .refused it outright until we added a minimal[build-system]block withhatchling. - Private index auth isn't picked up from
pip.conf. uv readsUV_INDEX_URL/UV_EXTRA_INDEX_URLand, for credentials, either the URL itself (https://user:token@host/simple) orkeyringif you setUV_KEYRING_PROVIDER=subprocess. The old~/.pip/pip.confwith a netrc entry is silently ignored, which cost us one confused CI run before we moved the token into a repo secret and an env var. - The Docker image needs the uv binary, not a pip install of it. We copy it in directly:
That cache mount is most of the win in CI -- the layer above it changes every build, but the uv cache persists across builds on the same runner.COPY --from=ghcr.io/astral-sh/uv:0.9 /uv /uvx /usr/local/bin/ RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev uv.lockis notrequirements.txt. Anything outside the uv/Docker world that still expects a plain requirements file (a couple of old deploy scripts, mainly) needsuv export --no-hashes -o requirements.txtrun explicitly; it doesn't happen automatically onuv lock.
Was it worth it
Yes, mostly for CI time and onboarding, not raw developer-machine speed -- on a warm machine the difference between pip install and uv sync is a few seconds either way. Where it paid for itself was Docker builds (down from roughly 90 seconds of dependency installation to under 10 with a warm cache mount) and new-contributor setup, which went from "install pyenv, install the right Python, create a venv, hope requirements.txt resolves" to just uv sync.

