# Type Hint Constraints

## Core Rule

**Use type hints on all public functions and methods**

## Function Type Hints

### Required on Public APIs

```python
# GOOD - complete type hints
def get_motion(self, motion_id: int) -> Optional[Dict]:
    ...

def get_filtered_motions(
    self,
    policy_area: str = "Alle",
    limit: int = 10
) -> List[Dict]:
    ...

def calculate_similarity(self, motion_a: int, motion_b: int) -> float:
    ...
```

### Optional Parameters

Use `Optional[X]` or `X | None`:

```python
# Both forms are acceptable
def get_motion(self, motion_id: Optional[int] = None) -> Optional[Dict]:
    ...

def get_motion(self, motion_id: int | None = None) -> dict | None:
    ...
```

### Multiple Return Types

Use `Union[X, Y]` or `|` operator:

```python
# Acceptable forms
def parse_value(self, value: str) -> Union[bool, str, None]:
    ...

def parse_value(self, value: str) -> bool | str | None:
    ...
```

### Generic Types

Use `List[X]`, `Dict[K, V]`, `Tuple[X, Y]`:

```python
from typing import Dict, List, Optional, Tuple

def get_motions(self, ids: List[int]) -> Dict[int, Dict]:
    """Map motion_id -> motion data."""
    ...

def process_batch(self, items: List[str]) -> Tuple[List[str], List[str]]:
    """Returns (successes, failures)."""
    ...
```

## Collection Types

Prefer specific types over bare `list`/`dict`:

```python
# GOOD - specific types
def get_votes(self) -> List[str]:
    ...

def get_metadata(self) -> Dict[str, Any]:
    ...

# ACCEPTABLE - for truly generic collections
def merge_dicts(*dicts: dict) -> dict:
    ...
```

## DuckDB Result Types

DuckDB returns tuples/lists - document expected structure:

```python
def get_motion(self, motion_id: int) -> Optional[Tuple]:
    """Returns (id, title, description, date, ...) or None."""
    conn = duckdb.connect(self.db_path)
    try:
        result = conn.execute(
            "SELECT * FROM motions WHERE id = ?", (motion_id,)
        ).fetchone()
        return result
    finally:
        conn.close()

# Or use Dict for clarity
def get_motion_as_dict(self, motion_id: int) -> Optional[Dict]:
    """Returns motion dict or None."""
    conn = duckdb.connect(self.db_path)
    try:
        row = conn.execute(
            "SELECT * FROM motions WHERE id = ?", (motion_id,)
        ).fetchone()
        if row:
            return {
                "id": row[0],
                "title": row[1],
                "description": row[2],
                ...
            }
        return None
    finally:
        conn.close()
```

## Class/Instance Types

Use `Self` for methods returning instance type:

```python
from typing import Self

class MotionDatabase:
    def with_connection(self, path: str) -> Self:
        """Return new instance with different path."""
        return MotionDatabase(db_path=path)
```

## Callback/Function Types

Use `Callable` for function parameters:

```python
from typing import Callable

def process_motions(
    motions: List[Dict],
    processor: Callable[[Dict], Any]
) -> List[Any]:
    return [processor(m) for m in motions]
```

## Type Aliases

Define clear type aliases for domain concepts:

```python
from typing import Dict, List, TypedDict, Literal

# Vote values
VoteValue = Literal["Voor", "Tegen", "Onthouden", "Geen stem", "Afwezig"]

# Policy areas
PolicyArea = Literal["Alle", "Economie", "Klimaat", "Immigratie", ...]

# Motion dict
class MotionDict(TypedDict):
    id: int
    title: str
    description: Optional[str]
    date: Optional[str]
    policy_area: Optional[str]
    voting_results: Optional[str]  # JSON string
    winning_margin: Optional[float]

def get_motion(self, motion_id: int) -> Optional[MotionDict]:
    ...
```

## Avoid `Any`

Use `Any` sparingly - prefer specific types:

```python
# AVOID - too vague
def process(data: Any) -> Any:
    ...

# PREFER - specific types
def process(motion: MotionDict) -> Optional[SimilarityResult]:
    ...
```

## Inline Type Hints

For simple cases, inline hints are fine:

```python
def get_count(self) -> int:
    ...

def is_empty(self) -> bool:
    ...
```

## Docstring Type Hints

For complex types, include in docstrings:

```python
def get_party_positions(self, window_id: str) -> Dict[str, List[float]]:
    """Get party positions in political space.
    
    Args:
        window_id: Time window (e.g., "2024-Q1")
    
    Returns:
        Dict mapping party_name -> [x, y] coordinates
    
    Example:
        >>> positions = db.get_party_positions("2024-Q1")
        >>> positions["VVD"]
        [0.5, -0.3]
    """
    ...
```

## Type Checking

For runtime type checking, use runtime checks:

```python
def set_count(self, count: int) -> None:
    if not isinstance(count, int):
        raise TypeError(f"Expected int, got {type(count).__name__}")
    self._count = count
```
