# Python Conventions

If a project-level Python style guide or repo-local `CONTRIBUTING.md` / `pyproject.toml` config exists, **it wins**. This file is the fallback.

## Core principles

- Write simple, explicit, readable Python. Prefer boring, maintainable code over clever abstractions.
- Optimize for correctness first, then clarity, then performance.
- Keep functions small and single-responsibility. Prefer composition over inheritance.
- Type-hint all public functions, class methods, and non-trivial internal functions.
- Use dataclasses or Pydantic models for structured data — never loose `dict`s for domain data.
- Make side effects explicit. Fail fast with clear errors. No hidden global state.
- Code must be testable without network, filesystem, database, or time dependencies.

## Python version

Target **Python 3.11+** unless told otherwise. Use modern syntax:

- `str | None`, not `Optional[str]`
- `list[str]` / `dict[str, int]`, not `List` / `Dict`
- `match` only when it improves clarity
- `pathlib.Path`, not string paths

## Style guide

PEP 8, enforced by `ruff format` (or `black`).

- Indent: **4 spaces**. Line length: **88** (ruff/black default) unless the project sets otherwise.
- `snake_case` functions/variables, `PascalCase` classes, `SCREAMING_SNAKE_CASE` constants, leading `_` for private.

## Function design

- One thing per function. Avoid bodies over ~40 lines without a strong reason.
- Explicit parameters over reading globals. Return values instead of mutating arguments.
- Avoid behavior-changing boolean flags — split into separate functions.
- Do not hide I/O inside functions that look pure. Do not catch broad exceptions unless re-raising with useful context.

Bad:

```python
def process(data, save=True, notify=False):
    ...
```

Better:

```python
def build_invoice(data: InvoiceInput) -> Invoice: ...
def save_invoice(invoice: Invoice) -> None: ...
def notify_invoice_created(invoice: Invoice) -> None: ...
```

## Typing

- Type hints everywhere meaningful. Avoid `Any` unless unavoidable; never silence a type error without a one-line why.
- No untyped dicts for domain data — use `dataclass`, `Enum`, `TypedDict`, or Pydantic.
- `Protocol` for dependency inversion.

Bad:

```python
def create_user(data: dict): ...
```

Better:

```python
@dataclass(frozen=True)
class CreateUserCommand:
    email: str
    name: str

def create_user(command: CreateUserCommand) -> User: ...
```

## Error handling

- Specific exception types with useful context. Never swallow exceptions silently.
- Do not use exceptions for normal control flow.
- Convert infrastructure errors into application/domain errors at boundaries.
- Never expose internal stack traces or secrets to users.

Bad:

```python
try:
    send_email(user)
except Exception:
    pass
```

Better:

```python
try:
    send_email(user)
except EmailProviderError as exc:
    raise NotificationFailedError(f"Failed to notify user_id={user.id}") from exc
```

## Async

- Use async only for real I/O concurrency. Never call sync-blocking I/O inside an async function.
- Always set timeouts on external calls. No fire-and-forget tasks without explicit lifecycle + error handling.

Bad:

```python
async def fetch():
    requests.get(url)          # blocking call inside async
```

Better:

```python
async def fetch(client: httpx.AsyncClient, url: str) -> Response:
    return await client.get(url, timeout=10)
```

## Project / module layout

Separate domain logic from infrastructure; keep business rules independent of frameworks, DBs, queues, and external APIs.

- `domain/` — entities, value objects, pure business logic
- `application/` — use cases, orchestration, commands, queries
- `infrastructure/` — DB, external APIs, filesystem, queues
- `interfaces/` — HTTP, CLI, workers, event handlers
- `tests/` — unit, integration, e2e

When the project is ports-and-adapters, also read [../architectures/hexagonal.md](../architectures/hexagonal.md).

## Security

- Never hardcode secrets — read from env vars or a secret manager.
- Parameterized SQL only; never build SQL via string interpolation / f-strings.
- Validate all external input. Treat file paths, URLs, headers, and serialized input as untrusted.
- No unsafe deserialization (arbitrary `pickle.load`).

## Database

- Explicit, reviewable SQL. Avoid N+1 queries. Wrap multi-step writes in transactions.
- Use repository interfaces (`Protocol`) when DB access should be decoupled from domain logic.
- Keep migrations backward-compatible when possible.

## Logging

- Structured logging — never `print()` for application logs.
- Include IDs/context (request, user, job, entity). Never log secrets, tokens, passwords, cookies, API keys, or sensitive PII.

## Tests

- Unit-test domain logic; integration-test DB, external-API wrappers, and framework wiring.
- Deterministic tests — no `sleep`, no execution-order dependence, no real external services unless marked integration/e2e.
- Mock only at boundaries. Use factories/builders for test data. Test behavior, not implementation.
- Every bug fix ships a regression test when feasible.

### Self-mock signals to refuse (rule from `clean-code.md` → Testing discipline)

- `unittest.mock.patch`-ing a method **on the class under test**, then asserting that method was called. Mock the collaborator it depends on, not the unit it *is*.
- Splitting out a helper *only so* the test can patch it, then asserting `helper.assert_called_once()` as the real check — that proves the SUT calls the helper, not that the behavior works.
- Reaching into privates (`obj._internal`) to assert on internal state instead of observable outcomes.
- A `MagicMock` standing in for the SUT itself with a `return_value` that mirrors the very thing under test.

What's fine: mocking injected dependencies (`Mock(spec=UserRepository)`, `httpx.MockTransport`), asserting on return values / raised exceptions / emitted events / boundary calls.

## Required tooling

Run before declaring a Python change complete:

- `ruff check .` — lint
- `ruff format --check .` — formatting (or `black --check .`)
- `mypy .` or `pyright` — type check (if configured)
- `pytest` — tests

Dependency/project management: `uv`, Poetry, or `pip-tools`. Pin dependencies in application projects; reach for the standard library before adding third-party deps, and avoid importing heavy deps at module-import time when unnecessary.

## Forbidden anti-patterns

God classes/functions, hidden global mutable state, circular imports, catch-all `except Exception` without re-raise, silent failure, untyped domain dicts, business logic in controllers/routes or coupled to ORM models, hardcoded secrets, string-formatted SQL, unbounded retries, missing timeouts on external calls, fire-and-forget async without error handling, order-dependent tests, abstractions before two concrete use cases, magic constants without names, copy-pasted logic, comments that restate obvious code, leftover `print()` debugging.
