The Python toolchain has finally converged. The last three years we got: uv replacing pip/venv/poetry/pyenv, Ruff replacing flake8/black/isort/pyupgrade, ty (and Pyrefly) joining mypy/pyright in the type-checker space. Builds are 10–100× faster. The result is a Python developer experience that, in 2026, is genuinely pleasant.
This post is the working setup. The minimum knowledge to run a modern Python project, and the rationale for each piece.
What’s in vs. what’s out
| Job | 2022 | 2026 |
|---|---|---|
| Install Python | pyenv | uv python install |
| Virtualenv | python -m venv | uv venv (auto) |
| Install deps | pip install | uv add / uv sync |
| Lock deps | pip-compile / poetry | uv.lock |
| Run script | python -m foo | uv run python -m foo |
| Lint | flake8, pylint | ruff check |
| Format | black, isort | ruff format |
| Upgrade syntax | pyupgrade | ruff check --select UP --fix |
| Type check | mypy | mypy / pyright / ty |
The big change isn’t features — it’s speed. uv installs deps in 100ms what pip did in 30s. Ruff lints a 100k-LoC repo in 200ms. The friction that made Python tooling unpleasant has just… gone away.
uv — the one tool to rule them all
Install once:
curl -LsSf https://astral.sh/uv/install.sh | sh
Now everything:
uv python install 3.13 # install a Python version
uv init my-app # new project (pyproject.toml + src layout)
cd my-app
uv add fastapi 'pydantic>=2' # add deps (writes pyproject.toml + uv.lock)
uv add --dev pytest ruff # dev deps
uv remove fastapi # remove
uv sync # install deps from lockfile, exact versions
uv run pytest # run a command in the project's venv
What this replaces:
pyenv→uv pythonpython -m venv+ manual activation →uvdoes it transparentlypip install -r requirements.txt→uv syncpip-compile/poetry lock→uv lockpipx→uv tool install
One binary, no Python required to install (it’s static), 100× faster than the union of what it replaces.
A real pyproject.toml
[project]
name = "my-app"
version = "0.1.0"
description = "A modern Python app"
readme = "README.md"
requires-python = ">=3.13"
authors = [{ name = "You", email = "[email protected]" }]
dependencies = [
"fastapi>=0.115",
"pydantic>=2.10",
"sqlalchemy[asyncio]>=2.0",
"asyncpg>=0.30",
"httpx>=0.28",
]
[project.optional-dependencies]
dev = [
"pytest>=8",
"pytest-asyncio>=0.24",
"ruff>=0.7",
"mypy>=1.13",
"pre-commit>=4",
]
[project.scripts]
my-app = "my_app.main:cli"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.uv]
package = true # makes `uv run` aware of your package
# ---- Ruff ----
[tool.ruff]
line-length = 100
target-version = "py313"
src = ["src"]
[tool.ruff.lint]
select = [
"E", "F", "W", # pycodestyle / pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"SIM", # flake8-simplify
"ASYNC", # async-correctness
"RUF", # ruff-specific
]
ignore = [
"E501", # line length (handled by formatter)
"B008", # function call in default argument (FastAPI Depends)
]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
line-ending = "auto"
# ---- mypy ----
[tool.mypy]
python_version = "3.13"
strict = true
plugins = ["pydantic.mypy"]
exclude = ["build", "dist"]
# ---- pytest ----
[tool.pytest.ini_options]
addopts = "-q --strict-markers"
asyncio_mode = "auto"
testpaths = ["tests"]
Notice:
- One file for project metadata, deps, lint config, type config, test config.
src/layout —src/my_app/. Prevents accidental imports of the project from the repo root.tool.uv.package = true— uv treats the project as a package, not just a venv.
Ruff — lint + format
Ruff replaces:
- flake8 (and most plugins)
- black (formatting)
- isort (import sorting)
- pyupgrade (modern syntax)
- autoflake (unused imports)
- bandit (security lints, partial)
ruff check . # lint
ruff check . --fix # auto-fix what's safe
ruff format . # format
The rule selection is the only thing that takes thought. The block in the pyproject above is a sane default for backend Python. Add more as you want stricter:
[tool.ruff.lint]
extend-select = [
"S", # security (bandit)
"PT", # pytest style
"TID", # tidy imports
"TCH", # typing-only imports
"ANN", # require type annotations
"PERF", # performance
"PL", # pylint subset
]
Ruff turns lint from “I’ll run it once before merging” into “the IDE auto-fixes on save, in milliseconds.” Different category of tool.
Type checking — mypy, pyright, or ty?
In 2026 you have three serious options:
| Tool | Speed | Ecosystem | When |
|---|---|---|---|
| mypy | Slow | Mature, plugins (Django, Pydantic) | Default; broadest plugin support |
| pyright | Fast | Best with VSCode | Editor / large codebases |
| ty (Astral) | Very fast | New (2025–2026) | Watch this space; great for CI |
| Pyrefly (Meta) | Very fast | New, type-inference focused | Watch this space |
For 2026, I’d:
- Use pyright in the editor (built into Pylance, fast, accurate).
- Use mypy in CI (best plugin ecosystem, especially for Django and SQLAlchemy).
- Try ty when it stabilizes — likely the future.
CI command:
uv run mypy src/
Type errors caught here are way cheaper than in production. Run on every PR.
Pre-commit hooks
# .pre-commit-config.yaml
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4
hooks:
- id: ruff
args: [--fix]
- id: ruff-format
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.5.4
hooks:
- id: uv-lock # ensure uv.lock matches pyproject
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-merge-conflict
- id: check-yaml
- id: check-added-large-files
Set up:
uv add --dev pre-commit
uv run pre-commit install
Now lint, format, and lockfile checks run on every commit. PR reviews stay focused on logic.
CI in 50 lines
# .github/workflows/ci.yml
name: ci
on: [pull_request, push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
with: { enable-cache: true }
- run: uv python install 3.13
- run: uv sync --all-extras
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy src/
- run: uv run pytest --cov=src --cov-report=xml
- uses: codecov/codecov-action@v4
with: { files: coverage.xml }
End-to-end Python CI in well under a minute on most repos. The enable-cache: true is uv’s Astral-maintained cache that makes warm runs absurd.
Project skeleton
my-app/
├── pyproject.toml
├── uv.lock
├── README.md
├── .pre-commit-config.yaml
├── .gitignore
├── src/
│ └── my_app/
│ ├── __init__.py
│ ├── main.py
│ └── ...
└── tests/
├── conftest.py
└── test_main.py
Why src/:
- Forces you to install your package (
uv sync) before testing it. You can’t accidentally import the working tree. - Keeps
pyproject.tomland tests at the top level where tools find them. - Decouples your package name from the repo name.
Migrating from older setups
From requirements.txt + venv
uv init # creates pyproject + .venv + uv.lock
uv add $(grep -v '^#' requirements.txt)
rm requirements.txt
From Poetry
uvx migrate-to-uv # community tool that converts pyproject.toml
Or by hand: copy [tool.poetry.dependencies] into [project] dependencies, run uv lock. The lock files differ but the deps don’t.
From Pipenv
There’s a pipenv-to-uv converter; honestly, just uv add everything from Pipfile and walk away. Pipenv’s resolver was famously slow; you’ll feel the upgrade.
Speed that changes behavior
uv add foo— typically 50–200ms.uv syncfrom lockfile — typically 100ms warm.ruff checkon a 100k-LoC repo — typically 100–500ms.ruff formaton the same — similar.
These numbers matter because they change behavior. When uv add foo finishes in a blink, you experiment more. When ruff format is instant, you don’t fight your formatter. When uv sync is 100ms, you run git pull && uv sync without flinching.
Tooling speed is a developer experience multiplier. This is the unsung Python win of 2024–2026.
Things still rough in 2026
- Lock-file-portable wheels. Cross-platform wheels are mostly fine, but exotic deps (some C extensions, GPU torch builds) still surprise you.
uvfor monorepos. Workspaces work, but the story is younger than Cargo’s. Watch the docs.pip install -e .semantics with namespaces. Get thesrc/layout right and most of these go away.- Type-checker disagreements. mypy and pyright disagree on edge cases. Use one as the source of truth in CI.
What I’d add as the project grows
hypothesisfor property-based tests once your domain types stabilize.scrivfor changelog management.triooranyioif you need a more controlled async story than asyncio’s defaults.logfire(Pydantic) orstructlogfor structured logs.pydantic-settingsfor typed env config.
Read this next
- Modern Python Tips — modern Python language features.
- Python Decorators Explained — patterns you’ll meet in any modern codebase.
- FastAPI + Pydantic v2 + SQLAlchemy 2.0 — these tools, applied to a real service.
If you want my full cookiecutter-style starter — pyproject.toml, pre-commit, CI, Dockerfile, FastAPI bootstrap, all wired up — it’s at rajpoot.dev
.
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .