--- name: python description: >- Use this skill when writing, linting, type-checking, testing, or packaging Python — including async code, CLI tools, bot cogs, and library packages. Covers strict typing with mypy, ruff configuration, virtualenv discipline, async patterns, and packaging. --- # Python — Rules & Preferences Extends [`coding-dev.md`](skills-public/coding-dev.md). Everything there applies; this adds Python specifics. For the surrounding build and deploy scripts, see [`bash.md`](skills-public/code-languages/bash.md). --- ## 1. The Gate ```bash ruff format --check . ruff check . mypy --strict . pytest ``` All four clean before anything is called done. Configure them in `pyproject.toml` so the editor, the CLI, and CI all read the same settings. --- ## 2. Typing — Strict **Every function gets annotated**, including `-> None`. Type hints are not optional documentation; they are the checker's input. ```toml [tool.mypy] strict = true warn_unreachable = true warn_return_any = true disallow_untyped_defs = true ``` - **Never bare `Any`.** Use a `Protocol`, a `TypeVar`, or `object` + narrowing. - `X | None` over `Optional[X]`; `list[str]` over `List[str]` (modern syntax, no `typing` import). - `TypedDict` or a dataclass for structured dicts — not `dict[str, Any]`. - `Literal` and `Enum` for closed sets of values, not bare strings. - `@dataclass(frozen=True, slots=True)` for value objects. - Third-party library missing stubs: install `types-*` if it exists, otherwise a narrowly scoped `# type: ignore[import-untyped]` with a comment — never a blanket module-level ignore. ### Value types over primitives The general rule is in [`coding-dev.md` §7](skills-public/coding-dev.md); Python's concrete choices are: - **`yarl.URL` for URLs**, not f-string assembly. It ships with `aiohttp`, so any async HTTP project already has it. `URL(base) / "api" / "v4" / "users"` joins without double slashes, and `.with_query({...})` escapes parameters that string formatting would corrupt. - **`pathlib.Path` for paths**, never `os.path` + `str`. Ruff's `PTH` rules enforce this; keep `os.fdopen` where a descriptor rather than a name is being adopted. - `enum.StrEnum` (3.11+) where a closed set must also serialize as a string, so the wire format stays a string without the code comparing bare literals. - `typing.NewType` for identifiers that should not be interchangeable — a `UserId` and a `RepoId` are both `int` and nothing otherwise stops them being swapped. - Parse at the boundary: take `str` in the public signature if callers supply one, convert immediately, and keep the real type internal. --- ## 3. Linting `ruff` is the linter and formatter (it replaces flake8, isort, black, pyupgrade, and most plugins). Enable a broad rule set rather than the default minimum: ```toml [tool.ruff.lint] select = ["E", "F", "W", "I", "N", "UP", "B", "A", "C4", "SIM", "RUF", "ASYNC", "S", "PTH"] ``` `S` (bandit security rules) and `ASYNC` are worth the noise. As always: fix, don't suppress — and when you must suppress, use a specific code (`# noqa: S603`) with a reason. --- ## 4. Environments - **Always a virtualenv or project-managed environment** (`uv`, `poetry`, `pdm`, `venv`). Never install into the system interpreter, and never `pip install --user` as a workaround for an immutable OS — use a container or a venv. - `uv` is the preferred installer/resolver where free choice exists; it is dramatically faster and lockfile-native. - Commit the lockfile. Pin the Python version in `pyproject.toml` (`requires-python`). - `pathlib.Path` over `os.path` everywhere (ruff's `PTH` rules enforce this). --- ## 5. Errors & Logging - **Specific exceptions only.** `except Exception:` needs a reason; bare `except:` is never acceptable (it catches `KeyboardInterrupt` and `SystemExit`). - **Never swallow silently.** If you catch and continue, log at `warning` or above. - `raise ... from err` to preserve the chain. - Custom exception types deriving from a single package-level base, so callers can catch the whole family. - **`logging` module, never `print`,** in anything that isn't a CLI writing to stdout as its actual output. One `logger = logging.getLogger(__name__)` per module, at module scope. - **Never log secrets** — tokens, auth headers, message content, personal data. --- ## 6. Async - `asyncio` with `async`/`await`. **Never block the event loop** — no `time.sleep`, no synchronous `requests`, no blocking file I/O in a coroutine. Use `asyncio.to_thread` for genuinely blocking calls. - `httpx` (or the framework's own client) over `requests` in async code. - **`asyncio.TaskGroup`** (3.11+) over bare `create_task` — it propagates exceptions and cancels siblings instead of losing them to an unawaited task. - **Every `create_task` result must be held in a reference**, or the garbage collector can cancel it mid-flight. - Cancellation is cooperative: clean up in `finally`, and never swallow `CancelledError`. - **Events over polling.** Subscribe to the library's event/callback API; do not loop on a timer. Where polling is genuinely unavoidable (no event source exists), use a **staggered backoff** rather than a fixed interval, and say why polling was necessary. --- ## 7. Rate Limiting & External Services When talking to a rate-limited API (a chat platform, a store, a tracker): - **Funnel outbound calls through a single queue/worker** rather than calling from arbitrary call sites. It is the only place a limit can actually be enforced. - **Backoff with jitter** on retry; never a tight retry loop. - Respect `Retry-After` and documented limits; assume undocumented ones exist. - **Be extremely careful scraping real consumer services.** Conservative delays, realistic headers, plenty of failsafes, and abort fast when a response looks unexpected. Assume you are one mistake away from a ban. --- ## 8. Structure - Package layout: `src//`, tests in `tests/`. - **Shared behavior goes in a real shared module**, imported by each consumer — not copy-pasted between sibling packages. When three components grow the same queue/retry/ formatting logic, that is the signal to extract it. - Reusable, generic functionality (a format parser, a protocol client) becomes **its own installable package** with its own repo and version, consumed as a dependency. - Keep persistent state in the framework's own config/storage system so it survives restarts; do not invent a parallel JSON file beside it. - File and function size limits from the general rules apply. Ruff has **no line-based** function-length rule — `PLR0915` (`too-many-statements`, default 50) and `C901` (cyclomatic complexity) are the closest proxies and are worth enabling, but neither measures lines. Put **both** the function-length and file-length limits in a pytest case (see [`coding-dev.md` §4](skills-public/coding-dev.md)). --- ## 9. Testing - `pytest`, with `pytest-asyncio` for coroutines. - Fixtures over setup/teardown methods; `tmp_path` for anything touching the filesystem. - **Regression test every fixed bug.** - Mock at the network boundary (`respx`, `responses`), not by monkeypatching your own internals — a test that patches the code under test proves nothing. - **Never exercise destructive operations against real accounts or real data.** Temporary fixtures only. - `pytest -p no:randomly` only when order genuinely matters, and note why.