# maple-py conventions Python conventions for the `maple-py/` package. These OVERRIDE default behavior; follow them exactly. (General repo guidance is in the root `CLAUDE.md`; 4-space indentation applies here too.) The package is pyright-checked (basic mode, Python 3.14). `Track` (`src/maple/track.py`) and the `Analysis` family (`src/maple/analyze.py`) are the reference examples for all of the below. ## Imports at the top of the file All imports go at module top. No lazy/local imports inside functions. Never wrap an import in `try/except ImportError` to degrade gracefully — if an integral dependency (e.g. `mixxx_analyzer`, `scipy`) is missing, the process must **die at startup** via the top-level import failing. The only acceptable indented import is an `if TYPE_CHECKING:` block to break a circular type-only reference. ## Fail loud — never swallow into None "Don't swallow into None" is a general principle at every level, including call sites. An operation that fails must **let the exception propagate** — never catch it and return `None` / an empty / degraded result. BPM/beat analysis is integral: `_mixxx_analyze` raises rather than returning None, and every `Analysis.analyze` call site (in `anlz.py`, `cli.py`, `collection/core.py`) lets failures abort the run (including not catching around `ThreadPoolExecutor` `fut.result()`). Reserve `None` returns for *legitimate absence* (e.g. a cache miss in `Analysis.load`), never for *failure*. The one tolerated catch is a genuinely *optional* side-effect failing (e.g. writing the optimization cache via `analysis.save()`), which logs and continues because it doesn't affect correctness. ## Specific types over generic ones - **Dataclasses, not bare dicts**, for structured values passed around. - **`pathlib.Path`, not `str`**, for local filesystem paths (strings are fine for serialization / rekordbox-USB paths that are inherently string data). - **`typing.NamedTuple` (or a small dataclass), not bare tuples**, for fixed records (e.g. `Beat(number, tempo_x100, time_ms)`, `Bands(low, mid, high)`). - **No `None`-valued fields.** Absent values resolve at instantiation to a concrete sentinel — `0.0` (0.0 = unknown), `""`, `[]`, `b""` — per `Track`. `None` is acceptable only as a *factory return* signalling absence, never as a stored field. Idiomatic dataclass shape: plain `@dataclass` (no frozen/slots), `@property` for computed views, `@classmethod from_*()` factories, concrete field defaults. ## Type every function parameter Always annotate every parameter, including nested/inner functions and `@classmethod` factory args (`self`/`cls` exempt). No bare `def f(res):`. Use the real type even from a third-party lib — e.g. `mixxx_analyzer.AnalysisResult` (referenced directly; no extra import needed under `from __future__ import annotations`).