Houndarr-specific Python conventions

SkillFiles & storage

Houndarr-specific Python conventions on top of the global python skill. Loads when reading or editing .py files in this repo. Covers the noqa/nosec suppression table, the AppSettings-as-plain-dataclass decision, the frozen-dataclass-with-slots invariant for domain models, the per-module logger pattern, and the background-task error-handling shape.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Houndarr-specific Python conventions skill

What this skill tells your AI

The instructions your AI receives, as published by av1155/houndarr in .agents/skills/houndarr-python/SKILL.md and read by ahel’s review.

Apply alongside the global python skill. The global skill covers PEP 604 unions, lowercase generics, mypy strict, naming, etc. This skill adds the Houndarr-specific decisions that are not generic Python good-practice.

Imports

from __future__ import annotations is mandatory in every .py file that contains code. Empty __init__.py package markers are exempt. isort runs via Ruff with known-first-party = ["houndarr"].

Naming conventions

KindStyleExample
Classes / dataclassesPascalCaseSonarrClient, AppSettings
Functions / methodssnake_casecreate_instance, run_instance_search
Private helpers_leading_underscore_write_log, _render
ConstantsUPPER_SNAKE_CASESESSION_MAX_AGE_SECONDS, SCHEMA_VERSION
Module-level state_leading_underscore_db_path, _runtime_settings
EnumsStrEnum, lowercase valuesInstanceType.sonarr
Type aliasesPascalCase or LiteralRunNowStatus = Literal["accepted", "not_found", "disabled"]

Dataclass invariants

  • AppSettings is a plain dataclass, not Pydantic. Pydantic is used only at the *arr wire boundary (src/houndarr/clients/_wire_models/), never for internal domain models or config. get_settings() is a lazy singleton and the only deliberately-mutable dataclass (env overrides applied in-place).
  • Every frozen dataclass uses slots=True. Domain models (MissingEpisode, LibraryMovie, etc.) live next to the client that builds them; Instance composes seven frozen sub-structs and is itself frozen and slotted. Callers evolve through dataclasses.replace.

Logging

Every module that logs uses logger = logging.getLogger(__name__) at module level. Root logger is configured in __main__.py via logging.basicConfig(). No alternative logging libraries (structlog, loguru) are used.

Error handling shape

  • Background tasks: except asyncio.CancelledError: raise first, then broad except Exception with # noqa: BLE001; log + continue/retry.
  • HTTP clients: response.raise_for_status() in _get() / _post(); callers catch httpx.HTTPError or httpx.TransportError.
  • Auth helpers: catch-all returns False (never leaks info).
  • Routes: return re-rendered templates with status_code=422 for validation errors; use HTTPException in API routes.

Docstrings

  • Module-level docstring on every file that contains code.
  • Google-style for functions: Args:, Returns:, Raises: sections.
  • Test functions may use brief single-line docstrings.

Comments

Read docs/commenting-standard.md at least once per session before writing or editing code in this repo. It codifies the full commenting standard (per-language rules for Python, HTML/Jinja2, CSS, JS, SQL, YAML, shell, Markdown) plus the universal principles. Core rule: comments explain why, code explains what.

Known noqa / nosec suppressions

CodeReason
SIM117Nested async with required by aiosqlite pattern
S104Intentional bind to 0.0.0.0 for self-hosted server
B008FastAPI Depends() in function defaults
S608 + nosecDynamic SQL with explicit column allowlist (4 files). Use bare # nosec (no test ID) per bandit#1204: the test-ID form emits a spurious WARNING nosec encountered (B608), but no failed test per f-string interpolation on bandit 1.7.3+
BLE001Broad exception in background loops (always with logging)
A002Parameter names type / id shadowing builtins (FastAPI form / function signature convention)
SLF001Test fixtures and __main__.py accessing private module state
PLW0603Module-level global reassignment (singletons); the PLW rule family is not currently selected in ruff config, so these comments are defensive / inert
S101Defensive assert in adapters and instance validation; also globally ignored in ruff config. Per-file comments are defensive.

Type annotations (Houndarr-specific reminders)

  • Tests are exempt from ANN rules (per-file-ignores in pyproject.toml).
  • collections.abc.AsyncGenerator, not typing.AsyncGenerator.
  • Specific error codes: # type: ignore[assignment]; never bare # type: ignore.

Signals

GitHub stars
284
Forks
4
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
houndarr-python
Source
github.com/av1155/houndarr
Houndarr-specific Python conventions: Skill · ahel