Skip to main content

loom-python

Python language expertise for idiomatic, production-quality code.

Source facts

Repository
cosmix/loom
Last source activity
September 24, 2026 at 17:08
Detected SKILL.md language
English
Stars
61
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
loom-python
description
Python language expertise for idiomatic, production-quality code.
triggers
["python","py","pip","uv","poetry","virtualenv","pytest","pydantic","fastapi","django","flask","pandas","numpy","dataclass","type hints","asyncio","mypy","ruff","black","sklearn","pytorch","tensorflow","jupyter","pipenv","conda"]
# Python Language Expertise ## Overview Idiomatic, production-grade Python: modern typing, async, packaging, and the web/data/ML ecosystem. Assumes competence — the value is in what is easy to get wrong (interning, mutable defaults, blocking the event loop, GIL, dataclass/Pydantic boundaries), not syntax tutorials. ## Tooling (uv / ruff / mypy) Ruff is a strong default for linting and formatting. `uv` is a capable project and environment manager, but preserve the repository's existing package manager and lockfile unless a migration is explicitly requested. **`uv`** — a single tool for project environments, locking, Python installation, and pip-compatible workflows. Use it for new work or repositories that already use it; do not treat it as an implicit replacement for Poetry or another established workflow. | Task | Command | | --- | --- | | New project | `uv init` (writes `pyproject.toml`) | | Add / remove dep | `uv add httpx` / `uv remove httpx` (updates `pyproject.toml` + `uv.lock`) | | Add dev dep | `uv add --dev pytest` | | Sync env from lock | `uv sync` (creates `.venv`, exact/reproducible) | | Run in project env | `uv run pytest` (auto-syncs first; no manual activate) | | Install a Python | `uv python install 3.12` | | Ephemeral tool | `uvx ruff check` (= `uv tool run`) | | pip shim | `uv pip install ...` (drop-in, no project file) | - In a uv-managed project, use `uv add`/`uv remove` so the manifest and `uv.lock` stay in sync; use `uv sync --frozen` in CI to fail on drift. **`ruff`** — single linter+formatter replacing flake8, isort, pyupgrade, pydocstyle, autoflake, and much of bandit. - `ruff check --fix` (lint+autofix), `ruff format` (near-identical to black, replaces it). - Config lives under `[tool.ruff.lint]` since 0.1 — top-level `select` is deprecated and warns. - Useful rule groups: `E`/`W` pycodestyle, `F` pyflakes, `I` isort, `UP` pyupgrade (keeps typing current), `B` bugbear (catches mutable defaults, `lru_cache` on methods), `SIM`, `S` bandit-security, `ASYNC`, `PL` pylint, `RUF`. Suppress with `# noqa: E501` (never bare `# noqa`). **`mypy --strict`** for type checking; `pyright`/`basedpyright` (what Pylance runs) is faster and stricter on inference. Always scope ignores: `# type: ignore[arg-type]`, never bare `# type: ignore`. ## Type Hints - 3.9+: built-in generics `list[T]`, `dict[K, V]`. 3.10+: `X | Y`, `X | None` (not `Union`/`Optional`). - Import `Callable`/`Sequence`/`Mapping`/`Iterator` from `collections.abc`, not `typing`. - **Accept abstract, return concrete:** take `Iterable`/`Sequence`/`Mapping` (max caller flexibility); return `list`/`dict` (callers know exactly what they get). - `Self` (3.11+) for fluent builders / alternate constructors. `TypedDict` for structured dicts (JSON payloads, `**kwargs`), `Protocol` for structural typing, `ParamSpec` for signature-preserving decorators — see Expert Practices. ```python from collections.abc import Iterable from typing import TypedDict, NotRequired, Self class MovieRow(TypedDict): # dict shape checkable by mypy title: str year: NotRequired[int] # 3.11+: optional key def deduplicate(items: Iterable[str]) -> list[str]: # abstract in, concrete out return list(dict.fromkeys(items)) class Builder: def with_name(self, name: str) -> Self: self._name = name return self ``` ## Async / Await Prefer `asyncio.TaskGroup` (3.11+) over `gather()`: on failure it cancels remaining sibling tasks, awaits them, and re-raises all errors as an `ExceptionGroup` (handled with `except*`). `gather()` does NOT cancel siblings on failure (they "won't be cancelled and will continue to run"), and `return_exceptions=True` silently mixes return values and exception objects into one list. Reserve `gather()` for genuinely independent fire-and-forget results. ⚠ **Never block the event loop.** A coroutine that calls blocking IO (`requests`, sync DB drivers, `open().read()`), `time.sleep()`, or CPU-heavy work freezes *every* task on that loop. Offload: `await asyncio.to_thread(fn, *args)` (3.9+) for blocking IO; `loop.run_in_executor(process_pool, fn)` for CPU work; `await asyncio.sleep()` not `time.sleep()`. See Async Gotchas for the full set. ```python import asyncio async def process_batch(urls: list[str]) -> list[dict]: async with asyncio.TaskGroup() as tg: tasks = [tg.create_task(fetch_data(url)) for url in urls] return [t.result() for t in tasks] # ExceptionGroup raised on any failure try: await process_batch(urls) except* ConnectionError as eg: # subgroup handling for exc in eg.exceptions: logger.error("fetch failed: %s", exc) rows = await asyncio.to_thread(cursor.execute, sql) # sync driver off the loop ``` ## Dataclasses vs Pydantic (the validation boundary) **Choose by trust, not habit.** | | `@dataclass` (stdlib) / `attrs` | Pydantic v2 `BaseModel` | | --- | --- | --- | | Validation | **None** — type hints are not enforced at runtime | Validates + coerces on construction | | Use for | Internal, already-trusted structs; hot paths | Untrusted edges: API bodies, config, external JSON | | Cost | Free | Per-instance validation (Rust core, but non-zero) | Don't trust a dataclass with external input (it will happily hold `age="banana"`); don't pay Pydantic's validation cost for internal structs constructed in a tight loop. For perf-critical validated decode, `msgspec` is the fastest option. ```python from dataclasses import dataclass, field from pydantic import BaseModel, Field, field_validator @dataclass(slots=True) # internal; slots=True saves memory + blocks typos class Config: host: str port: int = 8080 tags: list[str] = field(default_factory=list) # NEVER tags: list = [] class UserCreate(BaseModel): # untrusted edge email: str = Field(min_length=5) name: str = Field(max_length=100) @field_validator("email") @classmethod def norm_email(cls, v: str) -> str: if "@" not in v: raise ValueError("invalid email") # NEVER assert (-O strips it) return v.lower() ``` ⚠ Plain `@dataclass` sets `__hash__ = None` (unhashable — cannot be a set member / dict key). Use `frozen=True` for a hashable value object. `slots=True`/`kw_only=True` are 3.10+. ## Packaging (`pyproject.toml`) ```toml [project] name = "mypackage" version = "0.1.0" requires-python = ">=3.11" dependencies = ["httpx>=0.25", "pydantic>=2.0"] [dependency-groups] # PEP 735; `uv add --dev` targets this dev = ["pytest>=8", "pytest-asyncio>=0.23", "mypy>=1.9", "ruff>=0.4"] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.ruff.lint] # MUST be nested since ruff 0.1; top-level is deprecated select = ["E4", "E7", "E9", "F", "I", "UP", "B", "SIM"] [tool.mypy] strict = true [tool.pytest.ini_options] asyncio_mode = "auto" # else async tests are silently UNCOLLECTED (default "strict") ``` ## Web Frameworks ### FastAPI Type hints drive validation, serialization, and OpenAPI. Use `Annotated[..., Depends(...)]` for injection; `response_model` to shape/filter output. ```python from typing import Annotated from fastapi import FastAPI, Depends, HTTPException, status app = FastAPI() async def get_db() -> AsyncIterator[AsyncSession]: # yield-dependency: cleanup after response async with AsyncSession(engine) as s: yield s @app.post("/users/", response_model=User, status_code=status.HTTP_201_CREATED) async def create_user(user: UserCreate, db: Annotated[AsyncSession, Depends(get_db)]) -> User: return await UserService(db).create(user) ``` ⚠ Gotchas: - **`def` vs `async def` path ops:** a `def` handler runs in FastAPI's threadpool (blocking libs OK). An `async def` handler runs on the event loop — any blocking call inside stalls *all* requests. Pick `def` for sync ORMs, `async def` only with async libs. - `response_model` strips fields not on the model — the primary guard against leaking hashes/tokens; don't return the raw ORM object. - `Depends` results are cached per-request; use `BackgroundTasks` for post-response work. ### Django ```python from django.db import models, transaction class User(models.Model): email = models.EmailField(unique=True) class Meta: indexes = [models.Index(fields=["email"])] with transaction.atomic(): # rolls back on exception user = User.objects.create(email=e) Profile.objects.create(user=user) ``` ⚠ QuerySets are lazy (evaluated on iteration/`len`/slice). Kill N+1 with `.select_related()` (FK/1:1 join) and `.prefetch_related()` (M2M/reverse). Batch with `bulk_create`/`bulk_update`. `.only()`/`.defer()` to trim columns. ## Data & ML (dense reference) The APIs are well-known; these are the correctness/perf traps that bite in review. **pandas** - **Vectorize.** `df["c"] = df["a"] * df["b"]`, `.map`/`np.where`/masks — never `iterrows`/`itertuples`/row-wise `apply` (Python-level loop, 10–100× slower). - **Assign via `.loc`**, never chained (`df[m]["c"] = ...`) — raises `SettingWithCopyWarning` and may no-op. pandas 3.0 makes Copy-on-Write the default, ending the ambiguity. - Memory: `read_csv(usecols=, dtype=)`; nullable `Int64`/`boolean` for NA-bearing ints; `category` dtype for low-cardinality strings; chunk with `read_csv(chunksize=)` or move to `polars`/`dask`/`pyarrow` at scale. - `merge` silently fans out on non-unique keys (row explosion) — validate with `merge(..., validate="one_to_many")`. **numpy** - Slices are **views**, not copies — `b = a[1:3]; b[0] = 9` mutates `a`. `.copy()` to break aliasing. - Broadcasting + ufuncs over Python loops; watch silent `int` overflow on fixed-width dtypes. - Use `rng = np.random.default_rng(seed)` (Generator API), not legacy global `np.random.seed`/`np.random.rand`. **scikit-learn** - Wrap preprocessing + estimator in a `Pipeline` so scalers/encoders fit on the **train fold only** inside CV — fitting a scaler on the full dataset before splitting leaks test statistics and inflates scores. `ColumnTransformer` for heterogeneous columns. Set `random_state` for reproducibility. **PyTorch** - Training step order: `optimizer.zero_grad()` → forward → `loss.backward()` → `optimizer.step()`. Forgetting `zero_grad` accumulates gradients. - Eval: `model.eval()` (disables dropout, freezes BatchNorm running stats) **and** `torch.inference_mode()` / `torch.no_grad()` (no autograd graph). Both are needed; they do different things. - Accumulate metrics as `loss.item()`/`.detach()` — keeping tensors alive retains the whole graph → memory blowup. `DataLoader(num_workers>0, pin_memory=True)` for GPU throughput; move batches with `.to(device, non_blocking=True)`. ## Testing with pytest Async fixtures must use `@pytest_asyncio.fixture` (a plain `@pytest.fixture` on `async def` yields an un-awaited coroutine under strict mode). Session/module-scoped async fixtures need `loop_scope=`; the `event_loop` fixture was removed in pytest-asyncio 1.0. Built-in fixtures worth reaching for: - `tmp_path` (per-test `pathlib.Path` dir), `monkeypatch` (`setattr`/`setenv`/`chdir`/`syspath_prepend`, all auto-undone at teardown — never patch globals by hand), `capsys`/`caplog`, `pytest.raises(ValueError, match="regex")`. - `conftest.py` fixtures are auto-discovered by every test in that directory tree — no import. Fixture `scope=` is `function` (default) / `class` / `module` / `session`; `autouse=True` applies without being requested. - Run tips: `-x` stop on first fail, `-k EXPR` select, `--lf` last-failed, `-q`, parametrize `ids=` for readable names. ```python import pytest, pytest_asyncio @pytest_asyncio.fixture async def db_session() -> AsyncIterator[AsyncSession]: async with AsyncSession(engine) as s: yield s @pytest.mark.parametrize("email,valid", [("a@b.com", True), ("bad", False)]) def test_email(email: str, valid: bool) -> None: if valid: User(email=email) else: with pytest.raises(ValueError): User(email=email) def test_env(monkeypatch): monkeypatch.setenv("API_KEY", "test") # auto-restored after the test assert load_config().key == "test" ``` ## Loom Test Runner Adapter
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub