<!-- deft:deposit-link-rewrite v=1 source="content/languages/python.md" -->
# Python Standards

Legend (from RFC2119): !=MUST, ~=SHOULD, ≉=SHOULD NOT, ⊗=MUST NOT, ?=MAY.

**⚠️ See also** (load only when needed):
- [../main.md](../main.md) - General AI guidelines
- [../PROJECT.md](../../PROJECT.md) - For project-specific overrides
- [../coding/testing.md](../coding/testing.md) - When writing tests

**Stack**: Python 3.11+, pytest; Web: Flask/FastAPI; CLI: typer[all]; TUI: textual[dev]

## Standards

### Documentation
- ! PEP 257 docstrings for all public APIs

### Testing
See [../coding/testing.md](../coding/testing.md) for universal requirements.

- ! Use pytest + pytest-cov + pytest-mock
- Files: `test_*.py` or `*_test.py`

### Coverage
- ! ≥85% coverage
- ! Count src/\*
- ! Exclude entry points, scripts, generated code
- ! Display-bound entry points (pygame/tkinter/PyQt/Kivy/Electron event loops) cannot run headlessly — exclude them via `[tool.coverage.run] omit` so the threshold measures logic modules only (see `Headless GUI / event-loop testing` under Patterns)

### Style
- ! Follow PEP 8 via ruff + black + isort

### Type Hints
- ! Use PEP 484 type hints on all functions/methods
- ! Pass mypy strict mode

### Data Validation
- ~ Use Pydantic BaseModel for data crossing module/API boundaries
- ⊗ Raw dicts/lists for shared, persisted, or returned data
- ~ Use `strict=True`, `extra='forbid'` for data models
- ~ Use `frozen=True` for immutable shared data
- ~ Layered validation: type constraints → field validators → model validators
- ! Swarm/parallel work: strict + frozen mandatory

## Commands

See [commands.md](./commands.md).

## Patterns

### Testing
**Parametrize**: `@pytest.mark.parametrize("a,b",[(1,2)])`, add `ids=[]` for names
**Fixtures**: `@pytest.fixture; yield val; cleanup()` for setup/teardown
**HTTP**: Flask: `app.test_client()`; FastAPI: `TestClient(app)`
**Mock**: `mocker.patch("mod.X")` or `@patch("mod.X")`
**Class**: `@pytest.fixture(autouse=True)` in class for shared setup
**Property Testing**: `hypothesis` for property-based tests
**Factories**: `pydantic-factories` for test data generation

### Headless GUI / event-loop testing
Display-bound entry points (pygame, tkinter, PyQt/PySide, Kivy, Electron bridges) start an event loop that needs a real display, so they cannot execute under headless CI or a swarm-agent session. Measuring them drags overall coverage below the 85% threshold even when every logic module is fully tested — the agent reports an inflated per-session coverage that collapses when the full `src/` is measured. Two complementary patterns keep the gate honest.

**1. Headless display driver (test what you can).** For pygame/SDL, force the dummy video driver BEFORE importing the library so windowless smoke tests still run:
```python
import os

os.environ.setdefault("SDL_VIDEODRIVER", "dummy")  # ! set BEFORE importing pygame
os.environ.setdefault("SDL_AUDIODRIVER", "dummy")

import pygame  # now safe to import under headless CI
```
For tkinter, skip the test when no display is available:
```python
import pytest

tk = pytest.importorskip("tkinter")

def _has_display() -> bool:
    try:
        root = tk.Tk()
    except tk.TclError:
        return False
    root.destroy()
    return True

requires_display = pytest.mark.skipif(
    not _has_display(), reason="no display available (headless CI)"
)
```

**2. Coverage omit (exclude the unreachable loop).** Keep the blocking event loop in a thin entry-point module (e.g. `src/ui.py`) that holds no business logic, then omit it so the threshold reflects logic modules only:
```toml
[tool.coverage.run]
omit = [
  "*/tests/*",
  "*/venv/*",
  "*/.venv/*",
  "src/ui.py",        # display-bound pygame/tkinter event loop — cannot run headlessly (#1027)
]
```
- ~ Keep display-bound modules thin: push testable logic (state, scoring, input handling) into separate, fully-tested modules so only the event-loop shell is omitted
- ⊗ Omit a module that mixes business logic with the event loop — refactor the logic out first, then omit only the loop

### Data Models
**Pydantic BaseModel**:
```python
from pydantic import BaseModel, ConfigDict, Field, field_validator

class User(BaseModel):
    model_config = ConfigDict(
        strict=True,        # ! Type coercion off
        extra='forbid',     # ! Reject unknown fields
        frozen=True,        # ~ Immutable (recommended for shared data)
    )
    
    id: int = Field(..., gt=0)
    username: str = Field(..., min_length=3, max_length=32)
    
    @field_validator('username')
    @classmethod
    def validate_username(cls, v: str) -> str:
        if not v.isalnum():
            raise ValueError("username must be alphanumeric")
        return v
```

**Immutable Functional Style**:
```python
def process_order(order: Order) -> ProcessedOrder:
    # Immutable transformation
    data = order.model_dump()
    # ... pure transformations ...
    return ProcessedOrder(**data)
```

### Performance

**Validation:**
- ~ Use `model_validate_json()` over `json.loads()` + `model_validate()` (faster)
- ~ Reuse `TypeAdapter` instances for repeated validations
- ~ Use `fail_fast=True` on sequences to short-circuit bad items (Pydantic 2.8+)
- ~ Move heavy validators to Field constraints for hot paths

**Extreme Performance:**
- ? Consider `msgspec` for extreme perf needs (but prefer Pydantic for most)

**Telemetry:**
- See [../tools/telemetry.md](../tools/telemetry.md) for recommendations
- ~ Structured logging (structlog) for production
- ~ Sentry.io for error tracking
- ? logfire or OpenTelemetry for distributed tracing

## pyproject.toml

```toml
[project]
requires-python=">=3.11"
dependencies=["flask>=3.0.0"]  # or fastapi/typer[all]/textual[dev]
[dependency-groups]  # PEP 735 — uv syncs these automatically without extra flags
dev=["pytest>=7.4","pytest-cov>=4.1","pytest-mock>=3.12","hypothesis>=6.0","black>=23","isort>=5.12","ruff>=0.1","mypy>=1.7","pydantic>=2.0"]
[project.optional-dependencies]  # pip/hatch compat — requires: uv sync --extra dev / pip install -e ".[dev]"
prod=["pydantic>=2.0","logfire>=0.1"]  # ~ Observability for production
[tool.pytest.ini_options]
testpaths=["tests"]
python_files=["test_*.py","*_test.py"]
addopts="--cov=src --cov-report=html --cov-report=term-missing"
[tool.coverage.run]
omit=["*/tests/*","*/venv/*","*/.venv/*"]
[tool.coverage.report]
fail_under=85
[tool.black]
line-length=100
[tool.isort]
profile="black"
line_length=100
[tool.ruff]
line-length=100
select=["E","F","W","I","N","UP","B","A","C4","DTZ","T10","PIE","PT","RET","SIM"]
[tool.mypy]
python_version="3.11"
warn_return_any=true
warn_unused_configs=true
disallow_untyped_defs=true
```

## Hygiene

**Types:**
- ⊗ `# type: ignore` without an inline comment explaining exactly why it is safe
- ⊗ `Any` as a function return type where the concrete type is knowable
- ⊗ Bare `object` or untyped containers (`list`, `dict` with no generics) on public APIs

**Error handling:**
- ⊗ Bare `except:` or `except Exception: pass` — catch the specific exception; log or re-raise
- ⊗ Returning `None` or a neutral default to mask an exception — let it propagate

**Dead code:**
- ~ Run `vulture` to detect unused functions, classes, and variables
- ~ Add `vulture . --min-confidence 80` as a `task hygiene` target

**Circular dependencies:**
- ~ Run `pydeps <package>` or `importlab` to detect import cycles; resolve by extracting shared types to a lower-level module
- ⊗ Circular imports between modules — see [coding/hygiene.md](../coding/hygiene.md) for resolution pattern

## Compliance Checklist

- ! Follow PEP 257 (docstrings) and PEP 484 (type hints)
- ! See [../coding/testing.md](../coding/testing.md) for testing requirements
- ~ Use Pydantic for data validation
- ! Run `task check` before commit
