post python · 2025-05-04 · 5 min read
uv, ruff, pyright: the new Python toolchain that actually moves the needle
The Python toolchain in 2020 was: pip + venv + pip-tools for dependencies, flake8 + black + isort + pyupgrade for code style, mypy for types, build + twine for publishing. Seven tools, seven config files, seven flavours of slow.
The 2025 toolchain is three tools: uv, ruff, pyright (or basedpyright). All three are roughly 10–100× faster than their predecessors and replace multiple of them. This post is how I migrated, what each tool actually does, and what’s worth keeping from the old stack.
uv — replaces pip, venv, pip-tools, build, twine
uv is by Astral, written in Rust. It started as a pip replacement and has eaten the rest of the dependency-management surface.
# create a venv + install deps from pyproject.toml in one stepuv sync
# add a dependencyuv add fastapi
# add a dev-only dependencyuv add --dev pytest
# run a command in the venv (no `source .venv/bin/activate` needed)uv run pytest
# install Python itself, any version, no pyenv neededuv python install 3.13
# pin the Python version for this projectuv python pin 3.13What this replaces:
| Old tool | uv equivalent |
|---|---|
python -m venv .venv && source .venv/bin/activate | uv sync (creates implicitly) |
pip install -r requirements.txt | uv sync |
pip install fastapi && pip freeze > requirements.txt | uv add fastapi |
pip-compile requirements.in -o requirements.txt | uv lock (auto on add) |
pyenv install 3.13 | uv python install 3.13 |
python -m build && twine upload dist/* | uv build && uv publish |
The lockfile (uv.lock) is committed to git. Reproducible installs across machines, no more “works on my laptop, fails in CI”. Activate-the-venv ceremony goes away (uv run <cmd> handles it).
Speed: uv sync on a fresh repo with ~80 deps takes ~3 seconds. pip install -r requirements.txt for the same repo takes ~45.
ruff — replaces flake8, black, isort, pyupgrade, autoflake
ruff is also Astral. It’s a linter and a formatter in one binary, with the rule sets of the popular older tools merged in.
# format code (replaces black)ruff format
# lint code (replaces flake8 + isort + pyupgrade + many others)ruff check
# auto-fix what's saferuff check --fix
# CI mode: error on any findingruff check --no-fixThe configuration goes in pyproject.toml:
[tool.ruff]line-length = 100target-version = "py312"
[tool.ruff.lint]select = [ "E", # pycodestyle errors "W", # pycodestyle warnings "F", # pyflakes "I", # isort "B", # flake8-bugbear "UP", # pyupgrade "SIM", # flake8-simplify]ignore = [ "E501", # line length, handled by formatter]
[tool.ruff.format]quote-style = "double"indent-style = "space"What this replaces:
| Old tool | What it did | ruff rule code |
|---|---|---|
| flake8 | style errors | E, W |
| pyflakes | unused imports, undefined names | F |
| isort | import sorting | I |
| black | formatting | ruff format |
| pyupgrade | dict() → {}, list() → [], type-hint upgrades | UP |
| flake8-bugbear | likely-bugs detection | B |
| flake8-simplify | ”why not just write it shorter” | SIM |
Speed: ruff on a 50k-line codebase finishes in under a second. flake8 on the same takes 20-30. Black takes another 5-10.
pyright (or basedpyright) — replaces mypy
mypy was the standard for years. pyright (Microsoft, written in TypeScript) is faster, often more accurate, and ships with VS Code’s Pylance for free.
pyrightConfiguration in pyproject.toml:
[tool.pyright]include = ["src", "tests"]exclude = ["**/__pycache__", ".venv"]pythonVersion = "3.12"typeCheckingMode = "strict"reportMissingImports = "error"reportMissingTypeStubs = "warning"The typeCheckingMode = "strict" flag is aggressive. Strictness levels are off, basic, standard, strict. I default to strict for new projects and dial down per-file with # pyright: basic if needed.
basedpyright is a community fork of pyright with extra rules and a CLI that’s friendlier in CI. Same engine, slightly different defaults.
mypy still has its place — if you have a 100k-line codebase already on mypy with extensive # type: ignore comments, the migration is non-trivial. For new code, start with pyright.
A complete pyproject.toml for a modern project
[project]name = "myproject"version = "0.1.0"requires-python = ">=3.12"dependencies = [ "fastapi>=0.115", "pydantic>=2.6", "httpx>=0.27",]
[dependency-groups]dev = [ "pytest>=8.0", "pyright>=1.1.380", "ruff>=0.6.0",]
[build-system]requires = ["hatchling"]build-backend = "hatchling.build"
[tool.ruff]line-length = 100target-version = "py312"
[tool.ruff.lint]select = ["E", "W", "F", "I", "B", "UP", "SIM"]
[tool.pyright]include = ["src", "tests"]typeCheckingMode = "strict"That’s it. One config file. No requirements.txt, no setup.py, no tox.ini, no .flake8, no .isort.cfg, no mypy.ini. The whole project is described in pyproject.toml.
Migration playbook
For a project currently on pip + flake8 + black + isort + mypy:
Step 1: install uv and let it adopt the project.
brew install uv # or curl -LsSf https://astral.sh/uv/install.sh | shcd myprojectuv init --no-readme --no-package # if no pyproject.toml yetIf you already have a requirements.txt:
uv add $(cat requirements.txt)rm requirements.txt requirements-dev.txtVerify: uv sync && uv run pytest should pass.
Step 2: replace black + flake8 + isort with ruff.
uv add --dev ruffruff check --fix .ruff format .Add the [tool.ruff] config from above to pyproject.toml. Delete .flake8, .isort.cfg, the [tool.black] section. Update CI to call ruff check and ruff format --check.
Step 3: try pyright (optional, depending on mypy investment).
uv add --dev pyrightpyrightTriage the new findings. pyright catches things mypy misses (and vice versa); expect a small wave of new errors on the first run. Fix or # pyright: ignore per case.
Step 4: clean up the old config files.
rm requirements.txt requirements-dev.txt setup.py setup.cfg .flake8 .isort.cfgAnything that’s now in pyproject.toml deletes from the repo root.
What’s worth keeping from the old stack
- pytest. ruff and pyright don’t replace it. Still the test runner of choice.
- mkdocs / sphinx for docs. Neither uv nor ruff touches docs.
- pre-commit hooks. Run ruff and pyright on commit; same hooks, less noise.
What the new toolchain doesn’t solve
A few things still hurt:
- Cross-version compatibility. If your library supports 3.9–3.13, you still write the same conditional code. uv just makes installing those versions faster.
- Native dependencies.
uvis fast at resolving and installing, but if your dep needsgccand a C library, you still need to install those system-side. - Monorepos.
uv workspacesexists but is younger than its alternatives. For a multi-package monorepo, evaluate carefully.
Closing
Three tools, three config sections in one file, one lockfile, one venv per project, no activation ceremony. Type checking finishes in seconds. Linting finishes in milliseconds. Dependency installs are 10× faster.
The toolchain pays for itself within a sprint. The migration is two afternoons. There’s no good reason to be on the old stack in 2025 unless you have a 100k-line legacy codebase that hasn’t budgeted the migration yet.