# 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. ```python 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(...)`). ```python 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](https://github.com/pydantic/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: ```python 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`. ```python 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 ```python 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](async-anyio.md). The summary: ```python 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. ```python 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](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. ```python 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](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: ```bash 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](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](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](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](data-processing.md). ## Tests — pytest Plain `unittest` is fine for stdlib; everything else uses pytest. Conventions: - File names `test_*.py`, function names `test_*`. - Fixtures via `@pytest.fixture`. Async fixtures are anyio-aware (`@pytest.fixture` on an async function works under `pytest-anyio` which is bundled with anyio). - Parametrise with `@pytest.mark.parametrize`. - Mark async tests with `@pytest.mark.anyio` (provided by anyio's pytest plugin). ```python 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`: ```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. ```python 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`. ```python 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.