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
| Purpose | Tool | Replaces |
|---|---|---|
| Package management | uv | pip, pip-tools, poetry, virtualenv |
| Linting & formatting | ruff | flake8, black, isort, pylint |
| Type checking | pyright / ty | mypy |
| Testing | pytest | unittest |
| Pre-commit | pre-commit + ruff | manual linting |
| Database migrations | alembic | Django 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.
Related posts Link to heading
All the detailed posts this workflow references:
Package management:
Code quality:
Testing:
Frameworks:
Database:
Environment: