Back to Oh My Openagent

Library Defaults — Decision Tree

packages/shared-skills/skills/programming/references/python/libraries.md

4.18.210.6 KB
Original Source

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

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": "[email protected]", "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:

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.

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.

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.

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 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": "[email protected]"}

@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.