post python · 2025-05-04 · 5 min read

uv, ruff, pyright: the new Python toolchain that actually moves the needle

#python#tooling#dx#productivity

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.

Terminal window
# create a venv + install deps from pyproject.toml in one step
uv sync
# add a dependency
uv add fastapi
# add a dev-only dependency
uv 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 needed
uv python install 3.13
# pin the Python version for this project
uv python pin 3.13

What this replaces:

Old tooluv equivalent
python -m venv .venv && source .venv/bin/activateuv sync (creates implicitly)
pip install -r requirements.txtuv sync
pip install fastapi && pip freeze > requirements.txtuv add fastapi
pip-compile requirements.in -o requirements.txtuv lock (auto on add)
pyenv install 3.13uv 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.

Terminal window
# format code (replaces black)
ruff format
# lint code (replaces flake8 + isort + pyupgrade + many others)
ruff check
# auto-fix what's safe
ruff check --fix
# CI mode: error on any finding
ruff check --no-fix

The configuration goes in pyproject.toml:

[tool.ruff]
line-length = 100
target-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 toolWhat it didruff rule code
flake8style errorsE, W
pyflakesunused imports, undefined namesF
isortimport sortingI
blackformattingruff format
pyupgradedict(){}, list()[], type-hint upgradesUP
flake8-bugbearlikely-bugs detectionB
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.

Terminal window
pyright

Configuration 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 = 100
target-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.

Terminal window
brew install uv # or curl -LsSf https://astral.sh/uv/install.sh | sh
cd myproject
uv init --no-readme --no-package # if no pyproject.toml yet

If you already have a requirements.txt:

Terminal window
uv add $(cat requirements.txt)
rm requirements.txt requirements-dev.txt

Verify: uv sync && uv run pytest should pass.

Step 2: replace black + flake8 + isort with ruff.

Terminal window
uv add --dev ruff
ruff 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).

Terminal window
uv add --dev pyright
pyright

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

Terminal window
rm requirements.txt requirements-dev.txt setup.py setup.cfg .flake8 .isort.cfg

Anything that’s now in pyproject.toml deletes from the repo root.

What’s worth keeping from the old stack

What the new toolchain doesn’t solve

A few things still hurt:

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.