A few years ago, setting up a Python project meant wiring together six or seven tools before writing a single line of code. Today that list has shrunk dramatically. The tools are faster, more ergonomic, and far simpler to configure.

The Old Python Tooling Setup

A "proper" Python project circa 2021 looked something like this:

pyenv install 3.10.4           # manage Python versions
python -m venv .venv           # create virtualenv
pip install -e ".[dev]"        # install deps (slow)
black .                        # format
isort .                        # sort imports (separate tool!)
flake8 .                       # lint
mypy .                         # type check

Seven commands, seven config sections. Each tool had its own config format. Sometimes tools needed a compatibility setting so they wouldn't step on each other's toes (for example black and isort). flake8 didn't support pyproject.toml without a plugin. Pre-commit required much config just to wire these together.

And releasing to PyPI? Even after pyproject.toml simplified packaging, you still need to choose a build backend like Hatchling, Flit, or Setuptools, and then, unless your tool has a built-in uploader, use Twine to publish.

uv, ruff, and ty: The Modern Stack

Nowadays we can just do:

uv init myproject && cd myproject
uv add fastapi sqlmodel
uv add --dev ruff ty pytest
uv run ruff check
uv run ruff format
uv run ty check
uv run pytest

uv handles Python versions, virtualenvs, dependency resolution, locking, and building. If you've written Rust, this will feel familiar: uv brings the cargo experience to Python. ruff replaces black, isort, and flake8 in a single binary. ty is the new type checker from the same Astral team and is much faster than mypy.

Here's a real pyproject.toml from one of my projects:

[project]
name = "mdweaver"
version = "0.2.0"
requires-python = ">=3.12"
dependencies = [
    "markdown>=3.5",
    "pygments>=2.17",
    "weasyprint>=60.0",
]

[dependency-groups]
dev = ["pytest>=8.0", "pytest-cov>=4.0", "ruff>=0.9.0", "tbump>=6.11.0", "ty>=0.0.12"]

[build-system]
requires = ["uv_build>=0.10.8,<0.11.0"]
build-backend = "uv_build"

No [tool.black], no [tool.isort], no [tool.mypy]. Ruff works with sensible defaults. One file configures everything. And notice the build backend: uv_build means even packaging is handled by uv — no separate hatchling or setuptools dependency needed.

Pre-commit Shrinks Too

The pre-commit config across my projects went from 30+ lines (black, isort, flake8, mypy hooks) to this:

repos:
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.6.4
    hooks:
      - id: ruff
        args: [--fix, --extend-select=I]
      - id: ruff-format

One tool does what three used to.

And if you want the hook runner itself to be fast too, prek is a Rust-based drop-in replacement for pre-commit. Same .pre-commit-config.yaml config, faster execution.

Releasing Got Simple Too

Shipping a package to PyPI used to mean a build backend, twine upload, and API token management. Now I use tbump (ok this is a separate tool) and trusted publishing.

$ uv run tbump 0.2.0
:: Bumping from 0.1.0 to 0.2.0
=> Would patch these files
- pyproject.toml:3 version = "0.1.0"
+ pyproject.toml:3 version = "0.2.0"
=> Would run these git commands
$ git add --update
$ git commit --message chore: bump version to 0.2.0
$ git tag --annotate --message v0.2.0 v0.2.0
$ git push --atomic origin main v0.2.0
:: Looking good? (y/N)
> y

One command bumps the version, commits, tags, and pushes. The tag triggers a GitHub Action that runs lint, tests, uv build, and publishes to PyPI. No API tokens to manage, no twine.

I learned this workflow from Automate Python package releases — worth reading if you publish packages.

A Note on Maturity

Back to the Astral stack. These tools are young. At the time of writing, ty is still in beta, and Astral is being acquired by OpenAI, which is causing mixed feelings in the community. The pace of development is fast, and the tools already work well for most projects. Since switching to uv I've not looked back.

Resources

The Python Developer Tooling Handbook has many useful short guides and tips, a resource to watch.

I spoke with creator Charlie Marsh on the Pybites Podcast about the design decisions behind these tools:

If you're still wiring together six tools to start a Python project, try this stack for your next one. The setup time alone will convince you.