packages/shared-skills/skills/programming/references/python/libraries.md
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.
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().
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)
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 (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.
Pydantic v2's core is in Rust (~10x faster than v1). It is the de-facto boundary validator. Use it for:
pydantic-settings)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.
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.
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.
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.
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
Textual builds rich, mouse-aware, mobile-style TUIs on the rich rendering engine. See textual-tui.md.
The agent framework from the Pydantic team. Type-strict, structured outputs are first-class, model-agnostic. See pydantic-ai.md.
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.
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.
Plain unittest is fine for stdlib; everything else uses pytest. Conventions:
test_*.py, function names test_*.@pytest.fixture. Async fixtures are anyio-aware (@pytest.fixture on an async function works under pytest-anyio which is bundled with anyio).@pytest.mark.parametrize.@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": "[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:
[tool.pytest.ini_options]
minversion = "8.0"
testpaths = ["tests"]
addopts = ["-ra", "--strict-config", "--strict-markers"]
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
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.