I’ve overhauled my Python development setup over the past year. The ecosystem has improved dramatically - faster tools, better defaults, and less configuration. This post describes my current workflow and links to the detailed posts on each tool.

The stack Link to heading

PurposeToolReplaces
Package managementuvpip, pip-tools, poetry, virtualenv
Linting & formattingruffflake8, black, isort, pylint
Type checkingpyright / tymypy
Testingpytestunittest
Pre-commitpre-commit + ruffmanual linting
Database migrationsalembicDjango migrations (for non-Django)

The theme: fewer tools, each doing more. uv replaced four tools. Ruff replaced three. Less config, fewer version conflicts, faster CI.

Package management with uv Link to heading

I’ve fully migrated from pip/poetry to uv. The benefits:

  • 10-100x faster installs (it’s written in Rust)
  • Lockfiles by default - reproducible installs without extra config
  • All-in-one - manages Python versions, virtual environments, and dependencies
  • Just works - fewer edge cases than poetry

Basic workflow:

# Create a new project
uv init my-project
cd my-project

# Add dependencies
uv add requests fastapi

# Run scripts
uv run python script.py
uv run pytest

See Running scripts with uv for more detail on uv run vs activating virtualenvs.

For private package registries: Publishing Python packages to Artifact Registry with uv.

For automated updates: Dependabot with uv.

Linting with ruff Link to heading

Ruff is absurdly fast. It runs all my linting in under 100ms where flake8 + black + isort took 10+ seconds.

# Check for issues
ruff check .

# Fix auto-fixable issues
ruff check --fix .

# Format code (replaces black)
ruff format .

Basic pyproject.toml config:

[tool.ruff]
line-length = 88
target-version = "py312"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B", "SIM"]

See ruff: the fast Python linter for setup and migration from flake8.

Type checking Link to heading

I use pyright for type checking - it’s faster and stricter than mypy. But ty from Astral (the ruff/uv folks) is worth watching. It’s still early but I’ve just started trying it in neovim.

The Astral toolchain (uv + ruff + ty) is shaping up to be the complete Python setup - all Rust-based, all fast, all with sensible defaults. Once ty matures, it’ll probably replace pyright entirely.

Pre-commit hooks Link to heading

I run ruff and a few other checks on every commit:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.8.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml

This catches issues before they hit CI. See Setting up pre-commit hooks for the full setup.

Testing with pytest Link to heading

My pytest setup focuses on fast feedback:

# Run only tests that failed last time
pytest --lf

# Run tests matching a pattern
pytest -k "test_auth"

# Stop on first failure
pytest -x

# Parallel execution
pytest -n auto

See pytest tips: –last-failed and specific tests for more shortcuts.

For async code: pytest-asyncio mode.

FastAPI for APIs Link to heading

FastAPI has become my default for new services. Automatic validation, OpenAPI docs out of the box, and proper async support.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class Item(BaseModel):
    name: str
    price: float

@app.post("/items/")
def create_item(item: Item):
    return item

See FastAPI basics for dependency injection, Pydantic models, and common gotchas.

If you’re migrating Pydantic models: Pydantic v2 migration tips.

Database migrations with alembic Link to heading

For non-Django projects, alembic handles schema migrations:

# Create a migration
alembic revision --autogenerate -m "add users table"

# Apply migrations
alembic upgrade head

# Check current state
alembic current
alembic history

See alembic history for debugging migration issues.

Environment setup Link to heading

I use direnv to automatically activate environments when entering project directories:

# .envrc
export VIRTUAL_ENV=.venv
export PATH="$VIRTUAL_ENV/bin:$PATH"

Watch out for Python path issues with pyenv/uv interactions - see direnv Python path issues.

CI pipeline Link to heading

A typical CI setup:

# .github/workflows/ci.yml
- name: Install uv
  uses: astral-sh/setup-uv@v4

- name: Install dependencies
  run: uv sync

- name: Lint
  run: uv run ruff check .

- name: Test
  run: uv run pytest

uv’s speed makes CI noticeably faster. No need for caching pip dependencies when installs take 2 seconds.

All the detailed posts this workflow references:

Package management:

Code quality:

Testing:

Frameworks:

Database:

Environment: