cloudapps and texsite both ran pip-compile inside every Docker build. Each image resolved dependencies from scratch, against whatever pip version was current. When pip-tools fell behind pip - different compatibility issues, months apart - builds broke. The fix each time was a version pin and a comment linking to the pip-tools issue tracker. The second pin was when I stopped patching and started replacing.
When the orchestrator is overhead
tox managed test environments in both projects. In texsite, the environment list was ruff and py313-dj42-wt63 - one lint environment, one test environment. A 1x1 matrix. tox's value proposition is running tests across Python versions and framework combinations. Without that matrix, it creates a virtualenv, installs extras, runs the test suite, and tears it down. In cloudapps, four environments, still one Python version. Same story.
The optional-dependency groups reflected that indirection:
[project.optional-dependencies]
linting = [
"mypy>=1.19,<2.0",
"ruff>=0.14,<1.0",
# ...
]
testing = [
"coverage>=7.13,<8.0",
"pytest>=9.0,<10.0",
"tox>=4.32,<5.0",
# ...
]
development = ["cloudapps[linting,testing]"]
Toml
Three extras so tox could install fine-grained subsets per environment. A development meta-extra composing the other two. tox itself listed as a testing dependency - the orchestrator was one of its own inputs.
What one lock file replaces
With uv, those three extras collapsed into one dependency group:
[dependency-groups]
dev = [
"coverage>=7.13,<8.0",
"mypy>=1.19,<2.0",
"pytest>=9.0,<10.0",
"ruff>=0.14,<1.0",
# ...
]
Toml
No tox. No meta-extra. PEP 735 dependency groups don't leak into package metadata - the artificial linting/testing split disappeared along with the reason it existed.
The cloudapps Dockerfile told the clearest story. Before:
RUN pip install --upgrade "pip<25.3" pip-tools
COPY pyproject.toml .
COPY README.md .
RUN pip-compile --output-file=requirements.txt pyproject.toml
RUN pip install --requirement requirements.txt
Dockerfile
A pinned pip (because pip-tools couldn't handle the latest), pip-tools installed at build time, dependencies resolved into a transient requirements.txt, then installed from that file. After:
COPY .python-version pyproject.toml uv.lock README.md ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --locked --no-dev --no-install-project
Dockerfile
--locked means uv reads the committed lock file and installs exactly those versions. No resolution at build time. The lock file is an artifact of development, not of the build.
tox.ini deleted in both projects. CI went from installing tox and delegating to tox -e <env> to calling tools directly with uv run - the commands visible where they run, not behind an indirection layer. Developer setup from three commands (python -m venv, activate, pip install -e .[development]) to one: uv sync.
The Ruff migration in 2023 replaced three linting tools with one. The toolchain cleanup last year deleted setup.py and setup.cfg. This step removed pip-tools and tox. Each round of consolidation deletes the coordination layer between the previous tools, not just the tools themselves. The extras-within-extras, the version pins, the tox environments that installed subsets of the same dependency graph - none of it was about the projects. It was about keeping the tools aligned with each other.