# Modern Python Toolchain

> 用 uv + ruff + pyright 搭建现代 Python 项目：初始化、依赖管理、环境配置、lint 与格式化。当用户要新建 Python 项目、配置 uv、设置 lint/format 时使用。 Modern Python project setup with uv, ruff, and pyright. Use when initializing a new Python project, configuring the Python environment, setting up linting/formatting, or when a project needs uv (the fast Python package manager). Trigger on: 'set up Python', 'new Python project', 'configure uv', 'install uv', 'ruff', 'pyright', 'Python linting', 'Python formatting', or when a task requires Python and no pyproject.toml exists yet.

- Skill: `practice019/modern-python-toolchain` (Agent Skill)
- Install (CLI): `npx skillmds@latest add practice019/modern-python-toolchain`
- Raw SKILL.md: https://api.skillmd.com/api/skills/practice019/modern-python-toolchain/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Productivity
- Author: Practice019 (https://skillmd.com/u/practice019)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/practice019/modern-python-toolchain

---


# Modern Python Toolchain

A guide for setting up Python projects with modern, fast tooling: **uv** (package/project manager), **ruff** (linter/formatter), and **pyright** (type checker).

## Installing uv

uv is an extremely fast Python package and project manager. It replaces pip, pip-tools, pipx, pyenv, virtualenv, poetry, etc.

```bash
# macOS / Linux
curl -LsSf https://astral.sh/uv/install.sh | sh

# Windows (PowerShell)
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"

# Homebrew (macOS)
brew install uv
```

After installation, restart your shell or run `source $HOME/.local/bin/env` (the installer prints the exact command).

For detailed information: https://docs.astral.sh/uv/

---

## uv basics

### Python version

Pin a single Python minor version. The recommended default is 3.12 (broadest ecosystem support — PyTorch, CUDA images, downstream libraries). Python 3.14 is the latest stable; prefer it for new projects unless you depend on packages that haven't added 3.14 support yet.

```toml
# pyproject.toml
requires-python = "==3.12.*"
```

Install Python via uv (no system Python needed):

```bash
uv python install 3.12
```

### Creating a new project

```bash
uv init                       # Create new project with pyproject.toml
uv init -p 3.12               # Specify Python version
```

### Common commands

```bash
uv add requests               # Add dependency
uv add --dev ruff "pyright[nodejs]"  # Add dev dependencies
uv remove requests            # Remove dependency
uv sync                       # Install from lockfile
uv run COMMAND                # Run command in project environment
uv run script.py              # Run a script
uv run python -c "..."        # Run Python one-liner
uvx TOOL ARGS                 # Run a tool without installing it
```

### Rules

- **Never use `pip`** in uv projects — always `uv add` for packages.
- **Never run `python script.py` directly** — always `uv run script.py` to ensure the correct environment. For one-liners use `uv run python -c "..."`.
- **Don't manually manage environments** with `python -m venv` or `source .venv/bin/activate` — uv handles this automatically.
- `uvx` runs tools from PyPI by package name without installing them permanently.

### Project types

For **library** projects (`uv init --lib`) or packaged apps (`uv init --package`), `uv_build` is used as the default build backend automatically:

```toml
[build-system]
# auto-generated by uv init; version bound tracks your installed uv (here: 0.11.28)
requires = ["uv_build>=0.11.28,<0.12.0"]
build-backend = "uv_build"
```

For **application** projects with an entry point:

```toml
[project.scripts]
myapp = "myapp.__main__:main"
```

If the project does not use src layout, just run `uv run main.py`.

---

## ruff

Ruff is an extremely fast Python linter and code formatter. It replaces Flake8, isort, Black, pyupgrade, autoflake, and more.

For detailed information: https://docs.astral.sh/ruff/

### When to use

Always use ruff for Python linting and formatting. Prefer `uv run ruff` when ruff is a dev dependency; otherwise fall back to `uvx ruff`.

### Configuration

Add to `pyproject.toml`:

```toml
[tool.ruff.lint]
extend-select = [
    "UP",  # pyupgrade
    "I",   # isort
]
```

Do not enable the full `E` category or other formatter-conflicting rules (E1xx, E501, W191, Q, COM); `ruff format` owns layout.

### Post-edit workflow

After modifying Python code, run both:

```bash
uv run ruff check --fix path/to/changed_file.py
uv run ruff format path/to/changed_file.py
```

Use `--diff` to preview changes without applying.

---

## pyright

Pyright is a fast type checker for Python. Only use it when the project lists it as a dev dependency or explicitly uses type checking.

Install with the `nodejs` extra so Node.js is bundled automatically (no system `node` required):

```bash
uv add --dev "pyright[nodejs]"
```

Run type checking:

```bash
uv run pyright path/to/changed_file.py    # check specific files
uv run pyright src/                        # check all code
```

Usually only check the files you modified. For broad changes (base classes, shared types), check the full tree.

---

## Coding style

### Type annotations

Use modern Python 3.12+ syntax:

```python
# Good — builtin generics, union syntax
def fetch(url: str, timeout: float = 30.0) -> list[dict[str, str | None]]:
    ...

# Bad — legacy typing imports
from typing import List, Dict, Optional
def fetch(url: str, timeout: float = 30.0) -> List[Dict[str, Optional[str]]]:
    ...
```

Always annotate function parameters. Local variables can rely on inference unless the type is ambiguous:

```python
items: list[tuple[str, int]] = []    # annotate — empty literal
config: dict[str, Any] = {}          # annotate — empty literal
result = some_api()                  # inference is fine
```

### pydantic v2

Use the modern class-based API:
- `model_config = ConfigDict(...)` at class body level, not `class Config`.
- `RootModel` with `root: SomeType` for single-root schemas.

### typer (CLI)

Recommended for CLI entry points over `argparse`:

```python
import typer
from typing import Annotated

cli = typer.Typer(add_completion=False)

@cli.command()
def main(name: Annotated[str, typer.Argument(help="Your name")]) -> None:
    typer.echo(f"Hello {name}")

if __name__ == "__main__":
    cli()
```

