sync-skills aggregates 4 component skills + 10 shared; 6 harness-tool skills get
the Claude Code Harness Tool Compatibility block; start-work's embedded Codex
section is demoted. Root hooks.json uses ${CLAUDE_PLUGIN_ROOT}, widens PostToolUse
to Write|Edit|MultiEdit, and registers no create_goal PreToolUse (D4). Plugin
aggregate test pins hooks/skills/mcp shape.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
11 KiB
Library Defaults — Decision Tree
For each domain, the canonical 2026 choice, why, and the canonical usage snippet. The skill enforces these unless the project's pyproject.toml explicitly says otherwise.
CLI — typer
typer builds a CLI from type-annotated function signatures. argparse needs 5x the code; click ignores type annotations; fire is magic that breaks at scale.
import typer
from rich import print as rprint
app = typer.Typer()
@app.command()
def greet(name: str, count: int = 1, shout: bool = False) -> None:
"""Print a greeting `count` times."""
message = f"Hello, {name}!" if not shout else f"HELLO, {name.upper()}!"
for _ in range(count):
rprint(message)
if __name__ == "__main__":
app()
For a single-function script, typer.run(main) skips the Typer() boilerplate. Subcommands use @app.command().
Terminal output — rich
rich produces tables, progress bars, syntax highlighting, traceback rendering. Use it for any structured output. Plain print is acceptable for non-interactive log lines (and even those are usually better via rich.console.Console(stderr=True).log(...)).
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Users")
table.add_column("ID", style="cyan")
table.add_column("Name", style="magenta")
table.add_row("1", "Alice")
console.print(table)
# Rich tracebacks (call once at process start)
from rich.traceback import install
install(show_locals=True)
HTTP client — httpx2
Next-generation HTTP client under Pydantic stewardship. Sync and async in one library, HTTP/2 native, brotli + zstd content decoding, real type stubs. Replaces requests (sync only), aiohttp (async only), and the original httpx.
Install: httpx2[http2,brotli,zstd] — always include all three extras, no exceptions.
A bare httpx2.AsyncClient() / httpx2.Client() is a bug. Always use the factory pattern from references/httpx2-optimization.md with ALL optimizations enabled by default:
import socket
import httpx2
# ── Production defaults — ALL ON, always. ──
_LIMITS = httpx2.Limits(max_connections=200, max_keepalive_connections=40, keepalive_expiry=30.0)
_TIMEOUT = httpx2.Timeout(connect=5.0, read=30.0, write=10.0, pool=10.0)
_SOCKET_OPTS: list[tuple[int, int, int]] = [(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]
# Async (the common case)
transport = httpx2.AsyncHTTPTransport(http2=True, retries=3, limits=_LIMITS, socket_options=_SOCKET_OPTS)
async with httpx2.AsyncClient(transport=transport, timeout=_TIMEOUT, follow_redirects=True) as client:
response = await client.get("https://api.example.com/users")
response.raise_for_status()
users = response.json()
# Sync
transport = httpx2.HTTPTransport(http2=True, retries=3, limits=_LIMITS, socket_options=_SOCKET_OPTS)
with httpx2.Client(transport=transport, timeout=_TIMEOUT, follow_redirects=True) as client:
response = client.get("https://api.example.com/users")
response.raise_for_status()
users = response.json()
See references/httpx2-optimization.md for the full factory functions (create_client() / create_async_client()), event hooks, and the rationale behind every setting. Load that reference whenever you write ANY network code.
JSON — stdlib json (default) or orjson (hot paths)
Stdlib json is fine for cold paths and configs. Reach for orjson when JSON is in the hot path — cache layers, queue payloads, streaming responses, structured logs, FastAPI endpoints returning raw dict / list.
import orjson
# orjson.dumps returns bytes, not str
raw: bytes = orjson.dumps(
payload,
option=orjson.OPT_NAIVE_UTC | orjson.OPT_UTC_Z | orjson.OPT_SERIALIZE_DATACLASS,
)
Critical 2026 fact: with Pydantic v2, model.model_dump_json() is backed by pydantic-core (Rust) and is faster than orjson + default= bridge for Pydantic-shaped responses. Use model_dump_json() for Pydantic; orjson for everything else.
For FastAPI: app = FastAPI(default_response_class=ORJSONResponse). Pydantic-typed responses bypass it (and that's correct — Pydantic's path is faster). Raw dict/list returns go through orjson.
See references/orjson-stack.md for the full decision tree, option flag reference, FastAPI integration, Redis/queue/logging patterns, and the model_dump_json() vs orjson benchmark.
Validation — pydantic v2
Pydantic v2's core is in Rust (~10x faster than v1). It is the de-facto boundary validator. Use it for:
- HTTP request/response models (FastAPI uses pydantic natively)
- Config files (env vars via
pydantic-settings) - Anything entering the program from outside
from pydantic import BaseModel, Field, EmailStr, field_validator
class User(BaseModel):
id: int = Field(ge=1)
email: EmailStr
name: str = Field(min_length=1, max_length=100)
age: int | None = Field(default=None, ge=0, le=150)
@field_validator("name")
@classmethod
def name_no_digits(cls, v: str) -> str:
if any(c.isdigit() for c in v):
raise ValueError("name cannot contain digits")
return v
# Inside the program, use the validated instance with confidence
user = User.model_validate({"id": 1, "email": "a@b.com", "name": "Alice"})
print(user.model_dump_json(indent=2))
@dataclass is fine for purely internal records (no validation needed). For anything crossing a process boundary, use Pydantic.
Async — anyio
Full reference: async-anyio.md. The summary:
import anyio
async def fetch(url: str) -> str:
await anyio.sleep(0.1)
return url
async def main() -> None:
async with anyio.create_task_group() as tg:
for url in ["a", "b", "c"]:
tg.start_soon(fetch, url)
anyio.run(main)
Never import asyncio directly. The third-party libraries you call are free to use asyncio internally.
Web framework — fastapi
Type-hint-driven HTTP framework. Pydantic models become OpenAPI schemas automatically.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class CreateUser(BaseModel):
name: str
email: str
class User(BaseModel):
id: int
name: str
email: str
@app.post("/users", response_model=User)
async def create_user(payload: CreateUser) -> User:
return User(id=1, **payload.model_dump())
Full stack with database: fastapi-stack.md.
ORM — sqlalchemy 2.x async
SQLAlchemy 2.x finally has a real async API. Use the modern declarative MappedAsDataclass style with type annotations.
from sqlalchemy import String
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, MappedAsDataclass
class Base(MappedAsDataclass, DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, init=False)
name: Mapped[str] = mapped_column(String(100))
email: Mapped[str] = mapped_column(String(255), unique=True)
engine = create_async_engine("postgresql+asyncpg://localhost/myapp")
SessionFactory = async_sessionmaker(engine, expire_on_commit=False)
Full pattern with FastAPI integration: fastapi-stack.md.
Database — postgres + asyncpg
For new applications, default to Postgres. SQLite for tests is fine; SQLite for production is not.
asyncpg is the fastest Python Postgres driver, native to SQLAlchemy 2.x async, native to FastAPI's lifespan model. URL: postgresql+asyncpg://user:pass@host:5432/db.
For migrations, use Alembic with [alembic.context] configured to use the async engine. Single-step:
uv add alembic
uv run alembic init -t async migrations
TUI — textual
Textual builds rich, mouse-aware, mobile-style TUIs on the rich rendering engine. See textual-tui.md.
AI agents — pydantic-ai
The agent framework from the Pydantic team. Type-strict, structured outputs are first-class, model-agnostic. See pydantic-ai.md.
DataFrames — polars + numpy
Polars is 10-50x faster than pandas, has a real type system, and supports lazy evaluation. Numpy stays in the toolbox for arrays. See data-processing.md.
OLAP / SQL — duckdb
DuckDB is the SQL engine for analytical workloads. Query CSV/Parquet/JSON files directly without loading into memory; perform joins and aggregations 3-4x faster than Polars; zero-copy interchange with Polars via Arrow. See data-processing.md.
Tests — pytest
Plain unittest is fine for stdlib; everything else uses pytest. Conventions:
- File names
test_*.py, function namestest_*. - Fixtures via
@pytest.fixture. Async fixtures are anyio-aware (@pytest.fixtureon an async function works underpytest-anyiowhich is bundled with anyio). - Parametrise with
@pytest.mark.parametrize. - Mark async tests with
@pytest.mark.anyio(provided by anyio's pytest plugin).
import pytest
import anyio
@pytest.fixture
def sample_user() -> dict[str, str]:
return {"name": "Alice", "email": "a@b.com"}
@pytest.mark.parametrize("count,expected", [(1, "Hello"), (2, "Hello, Hello")])
def test_greet(count: int, expected: str) -> None:
result = ", ".join(["Hello"] * count)
assert result == expected
@pytest.mark.anyio
async def test_async_fetch() -> None:
await anyio.sleep(0)
assert True
pyproject.toml:
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = ["-ra", "--strict-config", "--strict-markers"]
Settings / config — pydantic-settings
Loads env vars and .env files into a Pydantic model. Replaces ad-hoc os.environ.get(...) everywhere.
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_prefix="MYAPP_")
database_url: str
api_key: str = Field(min_length=1)
debug: bool = False
settings = Settings() # loads at import time; raises if any required var is missing
Logging — stdlib logging + rich handler
Stdlib logging is fine; it gets a face-lift from rich.logging.RichHandler.
import logging
from rich.logging import RichHandler
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True, show_path=False)],
)
log = logging.getLogger(__name__)
log.info("ready")
For structured logging in production, swap to structlog (separate dep). Don't roll your own.