INTRODUCTION
If you have written even a single line of Python in the last decade, you know the ritual. You set up a virtual environment, you run pip install, you wait. You write some code, then you run flake8, you wait. You run black, you wait. You run isort, you wait. You run pytest, you wait some more. Then you realize your linter and formatter are fighting over the same line of code.
The traditional Python toolchain—centered around pip, virtualenv, flake8, black, isort, and mypy—has served us admirably.
But let’s be honest: it feels slow because it is slow. These tools were built in an era of single-core CPUs and small scripts. Today, we work with monorepos, machine learning pipelines, and microservices where waiting 15 seconds for a linter feels like watching paint dry.
Enter the new guard. A new generation of tools, written not in Python, but in compiled languages like Rust and Go, is eating the Python toolchain’s lunch. They are not just incrementally faster; they are 10x to 100x faster. They combine multiple tools into one. And they are quietly becoming the new standard at companies like Google, Dropbox, and FastAPI.
In this guide, we will ditch the old guard—pip, flake8, black, isort, and even mypy in some cases—and replace them with three ultra-fast, modern alternatives:
- Ruff (the Flake8 + Black + isort + pyupgrade killer)
- uv (the pip + pip-tools + virtualenv killer)
- Pyright (the mypy killer, running at TypeScript speed)
Let’s tear down the old shed and build a new, blazing-fast Python workshop.
Part 1: Why the Old Toolchain Hurts (And Why You’ve Normalized It)
Before we celebrate the new, let’s diagnose the pain of the old. If you’ve never used anything but pip and flake8, you might not realize how much cognitive load and time you are losing.
The pip Problem: Resolving Dependencies in Molasses
pip is a marvel of engineering, but it was designed for a simpler time. When you run pip install, it performs dependency resolution linearly. It grabs one package, then its dependencies, then their dependencies. In a project with 200+ dependencies (hello, data science), this becomes a recursive nightmare.
- The “Resolving dependencies…” black hole: You’ve seen it. You hit enter, walk to get coffee, come back, and it’s still spinning.
- No lockfile by default:
pipfamously doesn’t produce a lockfile (pip freeze > requirements.txtis a lie — it doesn’t capture deep dependencies correctly). This leads to the “works on my machine” problem. - Virtual environment friction:
python -m venv .venvfollowed by.venv\Scripts\activate(Windows) orsource .venv/bin/activate(Mac/Linux) is boilerplate that interrupts flow.
The Flake8 Problem: Speed and Configuration Hell
Flake8 is actually a wrapper around three separate tools: PyFlakes, pycodestyle, and McCabe. That means:
- It’s slow. On a large codebase, Flake8 can take 5-10 seconds. Multiply that by every save and every commit hook.
- No auto-fix. Flake8 tells you what’s wrong but never fixes it. You need Black and isort for that.
- Plugin chaos. Want to lint for
print()statements left in code? That’s an extra plugin. Want to enforcefrom __future__ import annotations? Another plugin. Each plugin slows things down further. - It conflicts with Black. The classic dance:
flake8says line too long (79 chars),blacksays line should be 88 chars. You spend 15 minutes configuringsetup.cfgto make them shut up.
The MyPy Problem: Type Checking That Punishes You
MyPy is a beautiful idea: gradual typing in Python. But in practice:
- Cold start is glacial. First run of
mypyon a medium project can take 20+ seconds. - It’s a memory hog. Type inference across large codebases can eat 2-4 GB of RAM.
- Configuration fatigue. You need a
mypy.iniwith 20+ lines just to make it tolerate realistic code.
We have normalized waiting. We have normalized context-switching between five different tools. But we don’t have to anymore.
Part 2: Tool #1 – Ruff (The All-in-One Linter & Formatter)
Replaces: Flake8, Black, isort, pyupgrade, autoflake, pydocstyle, and 30+ plugins.
Written in: Rust.
Installation: pip install ruff (ironic, we know) or brew install ruff.
The Promise: One tool that lints, formats, sorts imports, and auto-fixes errors faster than you can blink.
Why Ruff Is a Game-Changer
Ruff isn’t just fast—it’s a complete rethinking of what a Python linter should be. Imagine if flake8, black, isort, and pyupgrade had a baby, and that baby was written by systems programmers who hate waiting.
Speed Comparison (Real-World Numbers)
Let’s take a typical Django monorepo with 15,000 lines of Python across 200 files.
| Tool | First Run | Second Run (cached) |
|---|---|---|
| Flake8 | 8.2 seconds | 7.9 seconds |
| Black | 4.1 seconds | 3.8 seconds |
| isort | 2.3 seconds | 2.1 seconds |
| Ruff (lint + format + isort) | 0.12 seconds | 0.04 seconds |
Yes, Ruff is 50x to 200x faster. But speed is just the headline. The real story is the integration.
One Config to Rule Them All
Instead of maintaining .flake8, pyproject.toml (for Black), .isort.cfg, and setup.cfg, Ruff uses a single pyproject.toml section:
[tool.ruff]
line-length = 88
target-version = "py312"[tool.ruff.lint]
select = [ “E”, # pycodestyle errors “W”, # pycodestyle warnings “F”, # Pyflakes “I”, # isort “C”, # mccabe complexity “B”, # flake8-bugbear “A”, # flake8-builtins “RET”, # flake8-return “SIM”, # flake8-simplify ] ignore = [“E501”] # let black handle line length
[tool.ruff.format]
quote-style = “double” indent-style = “space”
That’s it. No more guessing if your formatter and linter are in a cold war.
Auto-Fix That Actually Works
Ruff can fix hundreds of rules automatically. Run ruff check --fix and watch it:
- Remove unused imports (F401)
- Replace
type(None)withNone(F601) - Convert old-style
%formatting to f-strings (UP031) - Sort imports without a separate
isortstep - Add missing
# noqacomments intelligently
A real workflow:
$ ruff check . --fix
Found 34 errors (28 fixed, 6 remaining)
$ ruff format .
1 file reformattedNo more bouncing between three different CLI tools.
The Plugin Ecosystem (Without the Slowdown)
Ruff implements over 700 built-in rules, which cover:
- Flake8 builtins (A, B, C, E, F, W)
- flake8-bugbear (dangerous defaults, mutable arguments)
- flake8-comprehensions (unnecessary list/dict comprehensions)
- flake8-simplify (suggest simpler
if/elsestructures) - pydocstyle (docstring conventions)
- pyupgrade (automatic syntax upgrades for Python 3.7+)
And because Ruff is compiled, adding 700 rules doesn’t slow it down the way adding 10 plugins to Flake8 does.
Migration from Flake8 + Black + isort
Moving to Ruff is painless:
- Install Ruff:
pip install ruff - Generate config:
ruff ruleto see all rules, then copy the recommendedpyproject.tomlsection. - Test run:
ruff check --fix .(back up your code first!) - Replace pre-commit hooks:
# Before
- repo: https://github.com/pycqa/isort
- repo: https://github.com/psf/black
- repo: https://github.com/pycqa/flake8
# After
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.3.0
hooks:
- id: ruff
args: [--fix]
- id: ruff-formatWarning: Ruff’s formatter is not 100% compatible with Black. It follows the same spec but has minor differences in how it handles trailing commas and multiline expressions. For 99% of code, it’s identical. For the other 1%, you can pin Black for a transition period.
Part 3: Tool #2 – uv (The Pip-Killer from the Astral Team)
Replaces: pip, pip-tools, virtualenv, poetry (in some workflows), and pipenv.
Written in: Rust.
Installation: curl -LsSf https://astral.sh/uv/install.sh | sh
The Promise: A single binary that replaces pip, pip-compile, pip-sync, and virtualenv, with dependency resolution 10x faster than Poetry and 100x faster than pip.
The Problem With Pip (Let’s Really Look Under the Hood)
When you run pip install django, pip does this:
- Download
djangometadata - See
djangorequiresasgiref - Download
asgirefmetadata - See
asgirefrequires nothing - Download
sqlparse(if Django needs it) - Resolve versions (is
asgiref>=3.0compatible withdjango==4.2?)
This is depth-first resolution. In a project with 50+ dependencies, pip can make hundreds of HTTP requests and resolve the same package multiple times. It’s not pip’s fault—it’s the design of PyPI and the lack of a global lock.
Now imagine you have a requirements.in file with 20 top-level packages. Pip will spend 15 seconds resolving. uv does the same in 0.5 seconds.
How uv Achieves Ludicrous Speed
uv reuses three key insights from Rust’s Cargo package manager:
- Global cache: uv caches every wheel and metadata in
~/.cache/uv. The second time you install a package, it’s instant. - Parallel resolution: While pip resolves one package at a time, uv spawns threads to fetch metadata for all dependencies simultaneously.
- Pubgrub algorithm: Instead of pip’s backtracking resolver (slow, exponential in worst case), uv uses the same advanced dependency solver as Elixir’s mix and Dart’s pub. It’s SAT-solver-based and predictable.
Real-World uv Workflow
Creating a Virtual Environment (2 seconds instead of 10)
# Old way
$ python -m venv .venv
$ source .venv/bin/activate
(.venv) $ pip install --upgrade pip
# New way
$ uv venv
$ source .venv/bin/activate
(.venv) $ uv pip install --upgrade pip # still fasterBut uv venv does more: it creates a symlink to a global Python binary, saving disk space. On a machine with 10 projects, that’s 1 GB of saved disk.
Installing Dependencies (15 seconds → 0.8 seconds)
# pip
(.venv) $ pip install django fastapi uvicorn sqlalchemy alembic celery redis httpx
Resolving dependencies... (14.2 seconds)
Installing... (3.1 seconds)
# uv
(.venv) $ uv pip install django fastapi uvicorn sqlalchemy alembic celery redis httpx
Resolved 47 packages in 0.81 seconds
Installed 47 packages in 0.45 secondsThe Killer Feature: Lockfiles Without Poetry
Poetry became popular because it introduced poetry.lock. But Poetry is slow (written in Python) and opinionated (forces its project structure). uv gives you uv pip compile:
# Generate a lockfile from your loose requirements
$ uv pip compile requirements.in -o requirements.txt
Resolved 68 packages in 0.67 seconds
# Sync your environment to match the lockfile
$ uv pip sync requirements.txt
# Uninstalls extra packages, installs missing onesThis is pip-tools on steroids. And it’s 50x faster.
Advanced: Workspaces (Like pnpm for Python)
uv supports workspaces—multiple projects in one repo that share a single lockfile and virtual environment.
# pyproject.toml at monorepo root[tool.uv.workspace]
members = [“packages/auth”, “packages/api”, “packages/db”]
One uv lock resolves dependencies for all three packages simultaneously. One uv sync installs everything. Try doing that with pip without going insane.
Migrating from Pip and Pip-Tools
Step-by-step for a typical project:
- Install uv:
curl -LsSf https://astral.sh/uv/install.sh | sh - Create new venv:
uv venv(delete your old.venvfirst) - Compile dependencies: If you have
requirements.in, runuv pip compile requirements.in -o requirements.txt - Install:
uv pip sync requirements.txt - Update scripts: Change your
MakefileorDockerfilefrompip installtouv pip install
Docker example (this alone saves 2 minutes per build):
FROM python:3.12-slim
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
COPY requirements.txt .
RUN uv pip install --system -r requirements.txtCaveat: uv is not a drop-in replacement for pip in every scenario. Some edge cases (like installing from private repositories with custom index URLs) require you to pass --extra-index-url. But for 95% of projects, it just works.
Part 4: Tool #3 – Pyright (The Type Checker That Finally Feels Fast)
Replaces: mypy, pyre, pytype.
Written in: TypeScript (ironically) and runs on Node.js, but can be used as a Python CLI via pyright npm package or basedpyright.
The Promise: Type checking that runs in the background, updates incrementally, and feels like an extension of your editor, not a CI step.
The Mypy Problem (Revisited)
Mypy is slow because:
- It’s implemented in pure Python (slow loops for type inference)
- No incremental cache by default (you have to use
--cache-dirand pray) - Whole-project analysis on every run
On a codebase with 100,000 lines, mypy takes 25 seconds. pyright takes 1.8 seconds.
Why Pyright Wins
Pyright, despite being written in TypeScript and running on Node, has three advantages:
- Language server protocol (LSP) native: Pyright powers the Pylance extension in VS Code. That means your editor gets instant type checking as you type, not on save.
- Incremental analysis: Pyright only rechecks files that changed. After the first run, subsequent runs are nearly instant.
- Constraint solver: Pyright’s type system is more powerful than mypy’s. It handles conditional types,
@overloadbetter, and has a more correct understanding ofTypeVar.
Setting Up Pyright
Installation is weird (npm), but once done, it’s smooth:
npm install -g pyright
# or use basedpyright (Python wrapper)
pip install basedpyrightConfiguration (pyproject.toml again—notice the pattern?):
[tool.pyright]
include = ["src", "tests"]
exclude = ["**/__pycache__"]
pythonVersion = "3.12"
typeCheckingMode = "strict"
reportMissingImports = true
reportUnusedVariable = "error"Then run:
$ pyright
Found 12 errors in 3 files
No errors in 47 filesReal-World Comparison: Mypy vs. Pyright
Test on an open-source FastAPI project with 8,000 lines and 40 files.
| Metric | Mypy (v1.8) | Pyright (v1.1.350) |
|---|---|---|
| First run cold | 12.4 seconds | 2.1 seconds |
| Second run (no changes) | 9.8 seconds | 0.4 seconds |
| After single file change | 8.2 seconds | 0.2 seconds |
| Memory usage | 2.1 GB | 380 MB |
| Editor integration (VS Code) | Slow, lags | Instant, Pylance-backed |
The killer use case: Running type checks in pre-commit hooks. With mypy, you wait 10 seconds before commit. With pyright, you wait 1 second. That’s the difference between actually running it versus commenting it out.
Migration from Mypy
If you have an existing mypy.ini, convert it:
ignore_missing_imports→reportMissingImports = falsestrict_optional = True→ (default in pyright)warn_return_any→reportAny = false
Then run both side by side:
mypy src/ > mypy-output.txt
pyright src/ > pyright-output.txt
diff mypy-output.txt pyright-output.txtIn most cases, pyright finds more bugs, not fewer. But it may flag things mypy ignored (like uninitialized class attributes). Expect a one-time cleanup.
Caveat: Pyright does not support # type: ignore comments with [code] selectors (e.g., # type: ignore[attr-defined]). It only ignores the entire line. For most teams, this is fine.
Part 5: The Complete Modern Toolchain (Zero to Deploy)
Let’s put it all together into a single, coherent workflow that replaces your old Makefile, pre-commit config, and CI scripts.
Step 1: Project Init
# Create project with uv
uv venv
source .venv/bin/activate
uv pip install ruff pyrightStep 2: pyproject.toml (The Single Source of Truth)
[project]
name = "myapp"
version = "0.1.0"
dependencies = [
"fastapi>=0.100.0",
"sqlalchemy>=2.0",
][tool.ruff]
line-length = 88 target-version = “py312”
[tool.ruff.lint]
select = [“E”, “F”, “I”, “B”, “C4”, “UP”, “SIM”]
[tool.ruff.format]
quote-style = “double”
[tool.pyright]
include = [“src”] pythonVersion = “3.12” typeCheckingMode = “strict”
Step 3: Development Loop
# While coding (in one terminal)
$ uv run ruff check --watch . # auto-lint on save
# In another terminal
$ uv run pyright --watch # auto-type-check
# When ready to commit
$ uv run ruff check --fix .
$ uv run ruff format .
$ uv run pyright
$ uv run pytestStep 4: Pre-Commit Hook (Simplified)
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ruff-lint
name: ruff lint
entry: uv run ruff check --fix
language: system
types: [python]
- id: ruff-format
name: ruff format
entry: uv run ruff format
language: system
types: [python]
- id: pyright
name: pyright
entry: uv run pyright
language: system
types: [python]Step 5: CI Pipeline (GitHub Actions Example)
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Sync deps
run: uv pip sync requirements.txt
- name: Lint
run: uv run ruff check .
- name: Type check
run: uv run pyrightTotal CI time: often under 20 seconds for lint+type, down from 2 minutes.
Part 6: The Elephant in the Room – What About Poetry, PDM, and Hatch?
You might ask: “But I already use Poetry. Isn’t that modern?”
Poetry is modern compared to pip+virtualenv. But it’s slow. Poetry 1.7 takes 8 seconds to resolve dependencies on a medium project. uv takes 0.6 seconds. Poetry’s solver is written in Python; uv’s is written in Rust.
Should you switch from Poetry to uv? It depends:
- Switch if: You are tired of waiting, you want faster CI, you don’t use Poetry’s build system extensively.
- Stay if: You heavily use
poetry buildandpoetry publishwith custom plugins. uv doesn’t (yet) build wheels. You still needpython -m buildfor that.
The astral team (who make ruff and uv) is working on a full uv build command. By late 2025, uv may replace Poetry entirely.
Part 7: Potential Drawbacks & Honest Caveats
No tool is perfect. Let’s be real about the downsides.
Ruff’s Rough Edges
- Not 100% Black-compatible: If your team has Black in the commit hook, switching to Ruff’s formatter might cause churn. Run
ruff format --diffto see differences before switching. - Missing some niche plugins: If you rely on
flake8-pieorflake8-annotationsfor very specific rules, check if Ruff implements them (ruff rulecommand lists everything). - Young ecosystem: Ruff 0.0.x was buggy. Ruff 0.3+ is stable, but bugs still surface on weird Python syntax (like nested f-strings in lambdas).
uv’s Unfinished Features
- No
uv build: You still needpip install buildandpython -m buildto create source distributions. - No
uv publish: Usetwinefor now. - Private repositories: Setting up
--index-urlwith authentication is more manual thanpip(but works). - No lockfile for dev dependencies (yet): You have to compile two separate files:
requirements.txtandrequirements-dev.txt.
Pyright’s Annoyances
- Node.js dependency: Installing Node just for a Python type checker feels dirty. Use
basedpyright(Python wrapper that bundles Node) to avoid system Node. - VS Code bias: Works beautifully in VS Code. In PyCharm or Vim (via coc-pyright), it’s slightly less integrated.
- No
# type: ignore[code]: This is a real loss for teams that use granular ignores.
Conclusion: The Future Is Compiled (And That’s Fine)
Python is a slow language. That’s okay—it buys us readability and ease. But there is no reason that developer tooling for Python should also be slow. Tools like Ruff, uv, and Pyright prove that we can have the ergonomics of Python with the speed of Rust and TypeScript.
By migrating from pip → uv, you turn 15-second dependency installs into 1-second affairs.
By migrating from flake8+black+isort → ruff, you consolidate five config files and speed up linting by 100x.
By migrating from mypy → pyright, you get instant feedback in your editor and sub-second CI checks.
The total time saved per developer per day is easily 10-15 minutes of waiting for tools. For a team of 10, that’s 2.5 hours per day—more than a full week per month.
The old toolchain isn’t “bad.” It’s just obsolete. The new tools are ready. They are stable. And they are already used by projects like Django, FastAPI, and Pandas in their CI pipelines.
So go ahead. Uninstall flake8. Delete setup.cfg. Stop typing source .venv/bin/activate by hand. Install uv, ruff, and pyright. Then spend the time you saved doing something that matters—like writing actual Python code.
Quickstart Cheat Sheet
# Install the new toolchain
curl -LsSf https://astral.sh/uv/install.sh | sh
pip install ruff
npm install -g pyright # or pip install basedpyright
# New project
uv venv
source .venv/bin/activate
uv pip install ruff pyright
echo "[tool.ruff]" > pyproject.toml
echo "line-length = 88" >> pyproject.toml
# Daily commands
uv pip compile requirements.in # lock deps
uv pip sync requirements.txt # sync env
ruff check --fix . # lint & fix
ruff format . # format
pyright # type checkYour future self, waiting less for linters and resolvers, will thank you.




