open atlas
↑ Back to track
Python for JS/TS developers PY · 04 · 04

The 2025 toolchain: uv for environments and locking, ruff for lint and format, one pyproject.toml

uv collapses pip, venv, pipx and pip-tools into one Rust binary: a fast resolver, a universal lockfile, uv run and uvx. ruff replaces flake8, isort and black with one pass. pyproject.toml holds every config, and migration from legacy setups is incremental, not a rewrite.

PY Middle ◷ 17 min
Level
FoundationsJuniorMiddleSenior

The CI pipeline took fourteen minutes, and six of them were pip install. Another ninety seconds went to the lint stage, where flake8, isort, and black ran in sequence — and twice a month they fought: black would rewrap an import block, isort would “fix” it back, and the build failed on a diff no human wrote. Onboarding was worse. The README’s environment setup had eleven steps, two of them wrong since the Python 3.11 bump; new hires lost their first day to pip resolving for forty minutes and then failing on a transitive pin conflict that requirements.txt had no way to express. The migration took one engineer two days: uv sync restored the locked environment in eight seconds on a warm cache, uv run pytest made “works on my machine” mean the lockfile or nothing, and ruff check && ruff format replaced three quarreling tools with one pass that finished before the old stack had imported its plugins. The pipeline now runs in under five minutes; nobody has debugged a venv since.

uv: one binary where five tools used to be

uv (Astral, written in Rust) collapses pip, venv, pip-tools, pipx, and most of pyenv into one binary. The core loop: uv add httpx writes the dependency into pyproject.toml, resolves the full graph, and pins everything in uv.lock; uv sync makes the project’s .venv match the lockfile exactly — removing strays, not just adding; uv run pytest guarantees the command executes in that synced environment, creating it on the fly if a teammate just cloned the repo. uvx ruff runs a tool in an isolated, cached environment — pipx without the second tool. uv python install 3.12 even manages interpreters. Two properties do the heavy lifting. First, speed: the resolver is 10–100× faster than pip with a warm cache (Astral’s published benchmarks; cold installs are nearer 8–10×), which turns “resolve the whole graph from scratch” from a coffee break into a blink — that is what makes always-resolving-from-pyproject practical. Second, the lockfile: uv.lock is universal — resolved once for all platforms and Python versions, with hashes — so the Linux CI box and the macOS laptop install byte-identical graphs. The honest caveats: uv.lock is uv’s own format (a standard, PEP 751’s pylock.toml, is only now emerging), and the tool is young and VC-backed — but because all inputs live in standard pyproject.toml, the exit path back to pip is real.

# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx"]
# ///
# PEP 723 inline metadata: `uv run fetch.py` builds this one-off
# environment on demand — no requirements.txt for a single script.
import httpx
print(httpx.get("https://example.com").status_code)
Quiz

A teammate clones the repo and immediately runs uv run pytest with no setup. What happens?

ruff: lint and format in one pass

When lint lives in CI and takes 90 seconds, engineers stop caring about it. When it runs in under a second on every save, it becomes the same kind of instant feedback as a syntax error — and that changes which bugs get caught before review. That is the core argument for ruff.

ruff is the same consolidation applied to code quality: one Rust binary that re-implements flake8 (plus most of its popular plugins), isort, pyupgrade, and a large slice of pylint as ruff check, and a black-compatible formatter as ruff format (Astral measures the formatter as >99.9% identical to black on black’s own test corpus). Rules are opt-in by family in pyproject.toml: E/F (pycodestyle/pyflakes) as the baseline, I for import sorting, B for flake8-bugbear’s likely-bugs, UP for outdated syntax, with --fix auto-applying safe corrections. Speed is the headline number and it is not marketing: ruff lints CPython-scale codebases in well under a second where the flake8 stack took tens of seconds — fast enough to run the entire rule set on every save, which changes behavior: lint stops being a CI-only nag and becomes feedback you see while typing. The consolidation also ends the tri-tool fights from the Hook, because one tool with one config cannot disagree with itself about import wrapping. Tradeoffs to state: a residue of plugin-specific flake8 rules and some pylint checks have no ruff equivalent, and ruff intentionally does not type-check — it complements, never replaces, mypy/pyright.

pyproject.toml as the single config, and the migration path

Everything above reads its configuration from one file:

[project]
name = "billing"
requires-python = ">=3.12"
dependencies = ["httpx>=0.27", "pydantic>=2.7"]

[tool.ruff]
line-length = 100

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]

[tool.uv]
dev-dependencies = ["pytest>=8", "mypy>=1.10"]

One file replaces requirements.txt, requirements-dev.txt, setup.cfg, .flake8, .isort.cfg, and pyproject’s old black section. Migration from a legacy repo is incremental, and the order matters: (1) adopt ruff first — it is config-only, zero runtime risk; start with E,F, delete flake8/isort/black configs, expand rule families one PR at a time so the diff stays reviewable. (2) Use uv as a faster pip without changing workflow: uv pip install -r requirements.txt is a drop-in that proves the speed claim on your own repo. (3) Move dependencies into pyproject.toml with uv add, commit uv.lock, switch CI to uv sync --frozen so the build fails if the lockfile is stale instead of silently re-resolving. (4) Replace global tool installs with uvx. The anti-pattern is doing it as one big-bang PR that swaps packaging, linting, and formatting simultaneously — when something breaks, you cannot tell which layer did it.

Quiz

How does committing uv.lock differ from committing a pip freeze > requirements.txt snapshot?

Recall before you leave
  1. 01
    Walk through what uv add, uv sync, uv run, and uvx each do, and what makes uv.lock different from a pip freeze snapshot.
  2. 02
    What is the safe migration order from pip+flake8+isort+black, and why is the big-bang swap an anti-pattern?
Recap

The 2025 Python toolchain is a consolidation story. uv replaces pip, venv, pip-tools, pipx, and much of pyenv with one Rust binary: uv add resolves and pins into a universal, hash-verified uv.lock; uv sync makes the environment match it exactly; uv run guarantees commands execute inside that environment, building it on demand from a fresh clone; uvx runs tools in isolated caches; uv python install manages interpreters. Resolution runs 10–100× faster than pip on a warm cache, which is what makes lockfile-first workflows feel free, and the lockfile is resolved once for every platform — the end of “resolves differently on the CI box.” ruff applies the same consolidation to quality: ruff check re-implements flake8 and its plugin ecosystem, isort, and pyupgrade; ruff format is black-compatible to >99.9% on black’s own corpus; one pass finishes in well under a second at CPython scale, fast enough to move linting from CI nag to on-save feedback, and one config ends the black-vs-isort wars. Everything reads pyproject.toml — dependencies, rule selection, line length — replacing a half-dozen dotfiles. Migrate in risk order: ruff (config-only), uv as drop-in pip, then pyproject.toml plus committed lockfile with uv sync --frozen in CI, then uvx; never as one big-bang PR, because the first failure becomes unattributable. Caveats stay caveats: uv.lock is tool-specific until PEP 751 matures, ruff does not type-check, and a few flake8/pylint rules have no equivalent yet. Now when you see a CI pipeline that spends six minutes on pip install or a lint stage that fights itself on import ordering, you know the exact four-step migration that fixes it — and the exact reason to start with ruff, not with the lockfile.

Practice

Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.

recallapplystretch0 of 6 done
Connected lessons

Something unclear?

Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.

shortcuts expand
search
K
prev piece
k
next piece
j
cycle tier
t
this menu
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.