---
name: code-python-guidelines
description: >
  Apply Pysae Python coding guidelines when writing, reviewing, or refactoring Python in any
  Pysae repo: typing (3.14, mypy strict, no `from __future__`, PEP 695), imports (top-level,
  relative, no `__all__`), comments/docstrings, async-first, Pydantic/dataclasses, data-migration
  safety, logging, tests, and the ruff/black/mypy/pytest tooling conventions.
  Use when the user says /code-python-guidelines, /python-guidelines, 'guidelines python',
  'conventions python pysae', 'comment écrire du python ici', 'écris du python propre',
  'respecte les conventions python', 'revois ce code python', 'check python style',
  'python coding standards', 'how should I write Python here', 'follow the Python conventions',
  'review this Python', or writes/reviews any `.py` file in a Pysae project. Distinct from
  /code-quality, which actually runs the tests and linters rather than stating the rules.
allowed-tools:
  - Read
  - Grep
  - Glob
---

# Pysae Python Guidelines

These guidelines apply to **all** Pysae Python projects (API, workers, tooling, ai-tools, etc.).
Follow them whenever you write, review, or refactor Python code in a Pysae repository.

---

## 1. Python version & typing

- **Target: Python 3.14** — use all modern typing syntax natively.
- **Full type annotations required** — all functions, methods, variables, and parameters must be typed. `mypy --strict` must pass.
- **Never use `from __future__ import annotations`** — Python 3.14 does not need it.
- **Never use quoted/string type annotations** (e.g. `-> "MyClass"`) — use `Self`, `TYPE_CHECKING`, or other `typing` utilities instead. Forward references are resolved natively.
- **Use PEP 695 syntax** (Python 3.12+) for generics and type aliases — `type Alias = …`, `class C[T]: …`, `def f[T](...)` — instead of `TypeVar` / `TypeAlias`.
- **Timezone-aware datetimes only** — `datetime.now(UTC)` / `datetime.UTC`, never naive datetimes; ruff's `DTZ` rules enforce this.
- **Type external libraries** — install the `types-*` stubs (e.g. `types-requests`) for third-party libs that ship no inline types, so `mypy --strict` stays clean.
- **Prefer an enum over magic strings** — model any finite set of values (states, types, modes…) with a typed enum (`enum.StrEnum` in Python, `enum` in TypeScript), never a bare string constant (`X: str = "idle"`) nor a loose union of string literals. An enum names the intent and groups the values in one place. When forward-compatibility with a generated client that doesn't yet expose a value is required, declare a local enum covering the expected values and cast the raw value onto it, with a comment pointing to the source ticket.

## 2. Imports

- **Prefer top-level imports** — avoid local/deferred imports inside functions or methods. Use them only when strictly necessary to break circular imports or when the import is very heavy.
- **Prefer relative imports within packages** — use `from .module import X` or `from ..common.utils import X` instead of absolute `from pysae_something.common.utils import X`. Absolute imports are fine for script entry points (`if __name__ == "__main__"`).
- **Never use `__all__`** — do not define `__all__` in `__init__.py` or any other file. Import symbols explicitly where they are needed.
- **Re-export explicitly** — to re-expose a symbol from a package's `__init__.py`, prefer an explicit re-export `from .module import X as X` rather than a bare import or `__all__`. (Projects that set `implicit_reexport = true` in their mypy config may rely on plain imports instead.)

## 3. Comments & docstrings

Comments are minimal and earn their place. Add one **only** when:

- **It feeds public documentation** that matters to end users — e.g. a docstring/description on an API endpoint, a Pydantic field, or a CLI option/command that surfaces in OpenAPI, `--help`, or generated docs.
- **The intent is genuinely non-obvious** — a *why* that cannot be inferred from the code: a workaround, an ordering constraint, a subtle invariant, a non-obvious edge case.

Otherwise, leave it out:

- **Never describe the change or justify the edit** — `# replaced motor with pymongo`, `# fix for #1234`, `# added to handle X` all belong in the commit message / MR description / changelog, never in the code. Code reads as if written from scratch, with no history trail.
- **Never restate the code** — `# increment counter` above `counter += 1` adds nothing.
- **Calibrate on the file** — match the surrounding comment density; don't sprinkle comments into a file that deliberately has none, and don't strip the explanatory comments a dense file relies on.

## 4. Pydantic & data structures

- **Always use Pydantic models for FastAPI** — all request bodies, response payloads, and structured data must be Pydantic `BaseModel` subclasses. Never pass raw `dict` to/from endpoints or between layers.
- **Prefer typed dataclasses over raw dicts** — use `@dataclass` with explicit field types for internal data structures. Reserve `dict` for truly dynamic/schemaless data (e.g. JSON from external APIs before parsing).

## 5. Static files & resources

- **Use `importlib.resources`** for loading static files (HTML templates, CSS, JS) bundled in the package — store them in a `templates/` subdirectory and load with `importlib.resources.files(...)`. Never use `__file__`-relative paths.

## 6. Async-first

- **Prefer async code with `asyncio`** — all I/O-bound code (HTTP calls, database queries, file operations, subprocess calls) should be async. This applies to FastAPI services, workers, CLI tools, and data pipelines alike.
- **CLI tools must use `typer`** — use `asyncio.run()` to bootstrap async code from typer commands.
- **Use `async for` and `async with`** — for iterators and context managers that perform I/O (database cursors, HTTP streams, file handles).
- **Use `asyncio.gather` / `asyncio.TaskGroup`** for concurrent I/O — don't run awaitable calls sequentially when they are independent.

## 7. Logging & error handling

- **Don't log exceptions you re-raise** — if a `try/except` block re-raises the exception (or wraps it with `raise NewError(...) from e`), do not also call `logger.exception()`. The caller or the global exception handler will log it. Double-logging creates noise in monitoring.

## 8. Data migration safety

When code migrates data between stores (e.g. MongoDB → Redis, old collection → new collection):

- **Never delete from the source store during migration reads.** A migration triggered by a read should copy data to the new store but leave the source intact.
- **Source cleanup should be a separate, explicit operation** — a dedicated migration script or a later phase.
- **Both stores must coexist safely** during the migration period.
- **Kubernetes rolling update compatibility** — old pods (pre-migration) and new pods (post-migration) run simultaneously. The migration must not break old pods.
- Flag any code path where a read/get operation has a destructive side-effect — this is almost always a bug.

## 9. Tests

- **No module-level constants in tests** — any value shared across tests (paths, payloads, IDs…) goes through a `@pytest.fixture`, even a trivial one. A fixture makes the dependency explicit, allows per-test override, and scopes cleanly.
- **Type fixture parameters** — a test's fixture parameter is annotated with the fixture's declared return type.
- **Naming** — test classes start with `Test`, test methods with `test_`.
- **Mirror the package layout** — the tests tree mirrors the package tree (`pkg/foo/bar.py` → `pkg_tests/foo/test_bar.py`).

## 10. Code quality tooling

Every Pysae Python project uses these tools (configured in `pyproject.toml`):

| Tool       | Purpose                 | Command example |
|------------|-------------------------|-----------------|
| **ruff**   | Linter + import sorting | `ruff check`    |
| **black**  | Formatter               | `black`         |
| **mypy**   | Type checking (strict)  | `mypy`          |
| **pytest** | Tests                   | `pytest`        |

- Pre-commit hooks run ruff, black, and mypy automatically.
- **Pre-commit hooks must invoke tools with no arguments** — all checked packages, paths, and rules must be configured in `pyproject.toml` so that a bare `ruff check`, `black`, `mypy` works correctly.
- All four must pass before merging.
- **Read `pyproject.toml` before generating code** — check the ruff rules (`select`, `ignore`), black `line-length`, mypy `strict` settings, and any project-specific overrides. Generated code must comply with these settings out of the box, without requiring a format/lint fix pass afterwards.

$ARGUMENTS
