Back to skills
SkillHub ClubWrite Technical DocsFull StackBackendTech Writer

dignified-python

Python coding standards with automatic version detection. Use when writing, reviewing, or refactoring Python to ensure adherence to LBYL exception handling patterns, modern type syntax (list[str], str | None), pathlib operations, ABC-based interfaces, absolute imports, and explicit error boundaries at CLI level. Also provides production-tested code smell patterns from Dagster Labs for API design, parameter complexity, and code organization. Essential for maintaining erk's dignified Python standards.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
82
Hot score
93
Updated
March 20, 2026
Overall rating
C3.0
Composite score
3.0
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install dagster-io-skills-dignified-python

Repository

dagster-io/skills

Skill path: plugins/dignified-python/skills/dignified-python

Python coding standards with automatic version detection. Use when writing, reviewing, or refactoring Python to ensure adherence to LBYL exception handling patterns, modern type syntax (list[str], str | None), pathlib operations, ABC-based interfaces, absolute imports, and explicit error boundaries at CLI level. Also provides production-tested code smell patterns from Dagster Labs for API design, parameter complexity, and code organization. Essential for maintaining erk's dignified Python standards.

Open repository

Best for

Primary workflow: Write Technical Docs.

Technical facets: Full Stack, Backend, Tech Writer, Designer, Testing.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: dagster-io.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install dignified-python into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/dagster-io/skills before adding dignified-python to shared team environments
  • Use dignified-python for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: dignified-python
description:
  Python coding standards with automatic version detection. Use when writing, reviewing, or
  refactoring Python to ensure adherence to LBYL exception handling patterns, modern type syntax
  (list[str], str | None), pathlib operations, ABC-based interfaces, absolute imports, and explicit
  error boundaries at CLI level. Also provides production-tested code smell patterns from Dagster
  Labs for API design, parameter complexity, and code organization. Essential for maintaining erk's
  dignified Python standards.
---

# Dignified Python Coding Standards

## When to Use This Skill vs. Others

| If User Says...              | Use This Skill/Command  | Why                             |
| ---------------------------- | ----------------------- | ------------------------------- |
| "make this pythonic"         | `/dignified-python`     | Python code review needed       |
| "is this good python"        | `/dignified-python`     | Code quality assessment         |
| "LBYL vs EAFP"               | `/dignified-python`     | Exception handling patterns     |
| "type hints"                 | `/dignified-python`     | Modern typing guidance          |
| "pathlib vs os.path"         | `/dignified-python`     | Path handling patterns          |
| "best practices for dagster" | `/dagster-conventions`  | Dagster-specific patterns       |
| "implement X pipeline"       | `/dg:prototype`         | Ready to build, not just review |
| "which integration to use"   | `/dagster-integrations` | Integration discovery           |

## Core Knowledge (ALWAYS Loaded)

@dignified-python-core.md

## Version Detection

**Identify the project's minimum Python version** by checking (in order):

1. `pyproject.toml` - Look for `requires-python` field (e.g., `requires-python = ">=3.12"`)
2. `setup.py` or `setup.cfg` - Look for `python_requires`
3. `.python-version` file - Contains version like `3.12` or `3.12.0`
4. Default to Python 3.12 if no version specifier found

**Once identified, load the appropriate version-specific file:**

- Python 3.10: Load `versions/python-3.10.md`
- Python 3.11: Load `versions/python-3.11.md`
- Python 3.12: Load `versions/python-3.12.md`
- Python 3.13: Load `versions/python-3.13.md`

## Conditional Loading (Load Based on Task Patterns)

Core files above cover 80%+ of Python code patterns. Only load these additional files when you
detect specific patterns:

Pattern detection examples:

- If task mentions "click" or "CLI" -> Load `cli-patterns.md`
- If task mentions "subprocess" -> Load `subprocess.md`

## When to Read Each Reference Document

The `references/` directory contains detailed guidance for specialized topics. Load these on-demand
when you encounter relevant patterns:

### `references/exception-handling.md`

**Read when**:

- Writing try/except blocks
- Wrapping third-party APIs that may raise
- Seeing or writing `from e` or `from None`
- Unsure if LBYL alternative exists

### `references/interfaces.md`

**Read when**:

- Creating ABC or Protocol classes
- Writing @abstractmethod decorators
- Designing gateway layer interfaces
- Choosing between ABC and Protocol

### `references/typing-advanced.md`

**Read when**:

- Using typing.cast()
- Creating Literal type aliases
- Narrowing types in conditional blocks

### `references/module-design.md`

**Read when**:

- Creating new Python modules
- Adding module-level code (beyond simple constants)
- Using @cache decorator at module level
- Seeing Path() or computation at module level
- Considering inline imports

### `references/api-design.md`

**Read when**:

- Adding default parameter values to functions
- Defining functions with 5 or more parameters
- Using ThreadPoolExecutor.submit()
- Reviewing function signatures

### `references/checklists.md`

**Read when**:

- Final review before committing Python code
- Unsure if you've followed all rules
- Need a quick lookup of requirements

## How to Use This Skill

1. **Core knowledge** is loaded automatically (LBYL, pathlib, basic imports, anti-patterns)
2. **Version detection** happens once - identify the minimum Python version and load the appropriate
   version file
3. **Reference documents** are loaded on-demand based on the triggers above
4. **Additional patterns** may require extra loading (CLI patterns, subprocess)
5. **Each file is self-contained** with complete guidance for its domain


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### versions/python-3.10.md

```markdown
---
---

# Type Annotations - Python 3.10

This document provides complete, canonical type annotation guidance for Python 3.10. This is the
baseline for modern Python type syntax.

## Overview

Python 3.10 introduced major improvements to type annotation syntax through PEP 604 (union types via
`|`) and PEP 585 (generic types in standard collections). These features eliminated the need for
most `typing` module imports and made type annotations more concise and readable.

**What's new in 3.10:**

- Union types with `|` operator (PEP 604)
- Built-in generic types: `list[T]`, `dict[K, V]`, etc. (PEP 585)
- No more need for `List`, `Dict`, `Union`, `Optional` from typing

**What you need from typing module:**

- `TypeVar` for generic functions/classes
- `Protocol` for structural typing (rare - prefer ABC)
- `TYPE_CHECKING` for conditional imports
- `Any` (use sparingly)

## Complete Type Annotation Syntax for Python 3.10

### Basic Collection Types

✅ **PREFERRED** - Use built-in generic types:

```python
names: list[str] = []
mapping: dict[str, int] = {}
unique_ids: set[str] = set()
coordinates: tuple[int, int] = (0, 0)
```

❌ **WRONG** - Don't use typing module equivalents:

```python
from typing import List, Dict, Set, Tuple  # Don't do this
names: List[str] = []
mapping: Dict[str, int] = {}
```

**Why**: Built-in types are more concise, don't require imports, and are the modern Python standard.

### Union Types

✅ **PREFERRED** - Use `|` operator:

```python
def process(value: str | int) -> str:
    return str(value)

def find_config(name: str) -> dict[str, str] | dict[str, int]:
    ...

# Multiple unions
def parse(input: str | int | float) -> str:
    return str(input)
```

❌ **WRONG** - Don't use `typing.Union`:

```python
from typing import Union
def process(value: Union[str, int]) -> str:  # Don't do this
    ...
```

### Optional Types

✅ **PREFERRED** - Use `X | None`:

```python
def find_user(id: str) -> User | None:
    """Returns user or None if not found."""
    if id in users:
        return users[id]
    return None

def get_config(key: str) -> str | None:
    return config.get(key)
```

❌ **WRONG** - Don't use `typing.Optional`:

```python
from typing import Optional
def find_user(id: str) -> Optional[User]:  # Don't do this
    ...
```

### Generic Functions with TypeVar

✅ **PREFERRED** - Use TypeVar for generic functions:

```python
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T | None:
    """Return first item or None if empty."""
    if not items:
        return None
    return items[0]

def identity(value: T) -> T:
    """Return the value unchanged."""
    return value
```

**Note**: This is the standard way in Python 3.10. Python 3.12 introduces better syntax (PEP 695).

### Generic Classes

✅ **PREFERRED** - Use Generic with TypeVar:

```python
from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
    """A generic stack data structure."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> None:
        self._items.append(item)

    def pop(self) -> T | None:
        if not self._items:
            return None
        return self._items.pop()

# Usage
int_stack = Stack[int]()
int_stack.push(42)
```

**Note**: Python 3.12 introduces cleaner syntax for this pattern.

### Constrained and Bounded TypeVars

✅ **Use TypeVar constraints when needed**:

```python
from typing import TypeVar

# Constrained to specific types
Numeric = TypeVar("Numeric", int, float)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b

# Bounded to base class
T = TypeVar("T", bound=BaseClass)

def process(obj: T) -> T:
    return obj
```

### Callable Types

✅ **PREFERRED** - Use `collections.abc.Callable`:

```python
from collections.abc import Callable

# Function that takes int, returns str
processor: Callable[[int], str] = str

# Function with no args, returns None
callback: Callable[[], None] = lambda: None

# Function with multiple args
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i
```

### Type Aliases

✅ **Use simple assignment for type aliases**:

```python
# Simple alias
UserId = str
Config = dict[str, str | int | bool]

# Complex nested type
JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float | bool | None

def load_config() -> Config:
    return {"host": "localhost", "port": 8080}
```

**Note**: Python 3.12 introduces `type` statement for better alias support.

### when from **future** import annotations is Needed

Use `from __future__ import annotations` when you encounter:

**Forward references** (class referencing itself):

```python
from __future__ import annotations

class Node:
    def __init__(self, value: int, parent: Node | None = None):
        self.value = value
        self.parent = parent
```

**Circular type imports**:

```python
# a.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from b import B

class A:
    def method(self) -> B:
        ...
```

**Complex recursive types**:

```python
from __future__ import annotations

JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
```

### Interfaces: ABC vs Protocol

✅ **PREFERRED** - Use ABC for interfaces:

```python
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get(self, id: str) -> User | None:
        """Get user by ID."""

    @abstractmethod
    def save(self, user: User) -> None:
        """Save user."""
```

🟡 **VALID** - Use Protocol only for structural typing:

```python
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

# Any object with draw() method matches
def render(obj: Drawable) -> None:
    obj.draw()
```

**Dignified Python prefers ABC** because it makes inheritance and intent explicit.

## Complete Examples

### Repository Pattern

```python
from abc import ABC, abstractmethod

class Repository(ABC):
    """Abstract base class for data repositories."""

    @abstractmethod
    def get(self, id: str) -> dict[str, str] | None:
        """Get entity by ID."""

    @abstractmethod
    def save(self, entity: dict[str, str]) -> None:
        """Save entity."""

    @abstractmethod
    def delete(self, id: str) -> bool:
        """Delete entity, return success."""

class UserRepository(Repository):
    def __init__(self) -> None:
        self._users: dict[str, dict[str, str]] = {}

    def get(self, id: str) -> dict[str, str] | None:
        return self._users.get(id)

    def save(self, entity: dict[str, str]) -> None:
        if "id" not in entity:
            raise ValueError("Entity must have id")
        self._users[entity["id"]] = entity

    def delete(self, id: str) -> bool:
        if id in self._users:
            del self._users[id]
            return True
        return False
```

### Generic Data Structures

```python
from typing import Generic, TypeVar

T = TypeVar("T")

class Node(Generic[T]):
    """A node in a tree structure."""

    def __init__(self, value: T, children: list[Node[T]] | None = None) -> None:
        self.value = value
        self.children = children or []

    def add_child(self, child: Node[T]) -> None:
        self.children.append(child)

    def find(self, predicate: Callable[[T], bool]) -> Node[T] | None:
        """Find first node matching predicate."""
        if predicate(self.value):
            return self
        for child in self.children:
            result = child.find(predicate)
            if result:
                return result
        return None

# Usage
from collections.abc import Callable

root = Node[int](1)
root.add_child(Node[int](2))
root.add_child(Node[int](3))
```

### Configuration Management

```python
from dataclasses import dataclass

@dataclass(frozen=True)
class DatabaseConfig:
    host: str
    port: int
    username: str
    password: str | None = None
    ssl_enabled: bool = False

@dataclass(frozen=True)
class AppConfig:
    app_name: str
    debug_mode: bool
    database: DatabaseConfig
    feature_flags: dict[str, bool]

def load_config(path: str) -> AppConfig:
    """Load application configuration from file."""
    import json
    from pathlib import Path

    config_path = Path(path)
    if not config_path.exists():
        raise FileNotFoundError(f"Config not found: {path}")

    data: dict[str, str | int | bool | dict[str, str | int | bool]] = json.loads(
        config_path.read_text(encoding="utf-8")
    )

    # Parse and validate...
    return AppConfig(...)
```

### API Client with Error Handling

```python
from collections.abc import Callable
from typing import TypeVar

T = TypeVar("T")

class ApiResponse(Generic[T]):
    """Container for API response with data or error."""

    def __init__(self, data: T | None = None, error: str | None = None) -> None:
        self.data = data
        self.error = error

    def is_success(self) -> bool:
        return self.error is None

    def map(self, func: Callable[[T], U]) -> ApiResponse[U]:
        """Transform successful response data."""
        if self.is_success() and self.data is not None:
            return ApiResponse(data=func(self.data))
        return ApiResponse(error=self.error)

U = TypeVar("U")

def fetch_user(id: str) -> ApiResponse[dict[str, str]]:
    """Fetch user from API."""
    # Implementation...
    return ApiResponse(data={"id": id, "name": "Alice"})
```

## Type Checking Rules

### What to Type

✅ **MUST type**:

- All public function parameters (except `self`, `cls`)
- All public function return values
- All class attributes (public and private)
- Module-level constants

🟡 **SHOULD type**:

- Internal function signatures
- Complex local variables

🟢 **MAY skip**:

- Simple local variables where type is obvious (`count = 0`)
- Lambda parameters in short inline lambdas
- Loop variables in short comprehensions

### Running Type Checker

```bash
uv run ty check
```

All code should pass type checking without errors.

### Type Checking Configuration

Configure ty in `pyproject.toml`:

```toml
[tool.ty.environment]
python-version = "3.10"
```

## Common Patterns

### Checking for None

✅ **CORRECT** - Check before use:

```python
def process_user(user: User | None) -> str:
    if user is None:
        return "No user"
    return user.name
```

### Dict.get() with Type Safety

✅ **CORRECT** - Handle None case:

```python
def get_port(config: dict[str, int]) -> int:
    port = config.get("port")
    if port is None:
        return 8080
    return port
```

### List Operations

✅ **CORRECT** - Check before accessing:

```python
def first_or_default(items: list[str], default: str) -> str:
    if not items:
        return default
    return items[0]
```

## Migration from Python 3.9

If upgrading from Python 3.9, apply these changes:

1. **Replace typing module types**:
   - `List[X]` → `list[X]`
   - `Dict[K, V]` → `dict[K, V]`
   - `Set[X]` → `set[X]`
   - `Tuple[X, Y]` → `tuple[X, Y]`
   - `Union[X, Y]` → `X | Y`
   - `Optional[X]` → `X | None`

2. **Add future annotations if needed**:
   - Add `from __future__ import annotations` for forward references
   - Add for circular imports with `TYPE_CHECKING`

3. **Remove unnecessary imports**:
   - Remove `from typing import List, Dict, Optional, Union`
   - Keep only `TypeVar`, `Generic`, `Protocol`, `TYPE_CHECKING`, `Any`

## References

- [PEP 604: Union Types](https://peps.python.org/pep-0604/)
- [PEP 585: Type Hinting Generics In Standard Collections](https://peps.python.org/pep-0585/)
- [PEP 563: Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/)
- [Python 3.10 What's New - Type Hints](https://docs.python.org/3.10/whatsnew/3.10.html)

```

### versions/python-3.11.md

```markdown
---
---

# Type Annotations - Python 3.11

This document provides complete, canonical type annotation guidance for Python 3.11.

## Overview

Python 3.11 builds on 3.10's type syntax with the addition of the `Self` type (PEP 673), making
method chaining and builder patterns significantly cleaner. All modern syntax from 3.10 continues to
work.

**What's new in 3.11:**

- `Self` type for self-returning methods (PEP 673)
- Variadic generics with TypeVarTuple (PEP 646)
- Significantly improved error messages

**Available from 3.10:**

- Built-in generic types: `list[T]`, `dict[K, V]`, etc. (PEP 585)
- Union types with `|` operator (PEP 604)
- Optional with `X | None`

**What you need from typing module:**

- `Self` for self-returning methods (NEW)
- `TypeVar` for generic functions/classes
- `Generic` for generic classes
- `Protocol` for structural typing (rare - prefer ABC)
- `TYPE_CHECKING` for conditional imports
- `Any` (use sparingly)

## Complete Type Annotation Syntax for Python 3.11

### Basic Collection Types

✅ **PREFERRED** - Use built-in generic types:

```python
names: list[str] = []
mapping: dict[str, int] = {}
unique_ids: set[str] = set()
coordinates: tuple[int, int] = (0, 0)
```

❌ **WRONG** - Don't use typing module equivalents:

```python
from typing import List, Dict, Set, Tuple  # Don't do this
names: List[str] = []
```

### Union Types

✅ **PREFERRED** - Use `|` operator:

```python
def process(value: str | int) -> str:
    return str(value)

def find_config(name: str) -> dict[str, str] | dict[str, int]:
    ...

# Multiple unions
def parse(input: str | int | float) -> str:
    return str(input)
```

❌ **WRONG** - Don't use `typing.Union`:

```python
from typing import Union
def process(value: Union[str, int]) -> str:  # Don't do this
    ...
```

### Optional Types

✅ **PREFERRED** - Use `X | None`:

```python
def find_user(id: str) -> User | None:
    """Returns user or None if not found."""
    if id in users:
        return users[id]
    return None
```

❌ **WRONG** - Don't use `typing.Optional`:

```python
from typing import Optional
def find_user(id: str) -> Optional[User]:  # Don't do this
    ...
```

### Self Type for Self-Returning Methods (NEW in 3.11)

✅ **PREFERRED** - Use Self for methods that return the instance:

```python
from typing import Self

class Builder:
    def set_name(self, name: str) -> Self:
        self.name = name
        return self

    def set_value(self, value: int) -> Self:
        self.value = value
        return self

# Usage with type safety
builder = Builder().set_name("app").set_value(42)
```

❌ **WRONG** - Don't use bound TypeVar anymore:

```python
from typing import TypeVar

T = TypeVar("T", bound="Builder")

class Builder:
    def set_name(self: T, name: str) -> T:  # Don't do this
        ...
```

**When to use Self:**

- Methods that return `self`
- Builder pattern methods
- Fluent interfaces with method chaining
- Factory classmethods

**Self in classmethod:**

```python
from typing import Self

class Config:
    def __init__(self, data: dict[str, str]) -> None:
        self.data = data

    @classmethod
    def from_file(cls, path: str) -> Self:
        """Load config from file."""
        import json
        with open(path, encoding="utf-8") as f:
            data = json.load(f)
        return cls(data)
```

### Generic Functions with TypeVar

✅ **PREFERRED** - Use TypeVar for generic functions:

```python
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T | None:
    """Return first item or None if empty."""
    if not items:
        return None
    return items[0]

def identity(value: T) -> T:
    return value
```

**Note**: Python 3.12 introduces better syntax (PEP 695) for this pattern.

### Generic Classes

✅ **PREFERRED** - Use Generic with TypeVar:

```python
from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
    """A generic stack data structure."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> Self:  # Can combine with Self!
        self._items.append(item)
        return self

    def pop(self) -> T | None:
        if not self._items:
            return None
        return self._items.pop()

# Usage
int_stack = Stack[int]()
int_stack.push(42).push(43)  # Method chaining works!
```

**Note**: Python 3.12 introduces cleaner syntax for generic classes.

### Constrained and Bounded TypeVars

✅ **Use TypeVar constraints when needed**:

```python
from typing import TypeVar

# Constrained to specific types
Numeric = TypeVar("Numeric", int, float)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b

# Bounded to base class
T = TypeVar("T", bound=BaseClass)

def process(obj: T) -> T:
    return obj
```

### Callable Types

✅ **PREFERRED** - Use `collections.abc.Callable`:

```python
from collections.abc import Callable

# Function that takes int, returns str
processor: Callable[[int], str] = str

# Function with no args, returns None
callback: Callable[[], None] = lambda: None

# Function with multiple args
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i
```

### Type Aliases

✅ **Use simple assignment for type aliases**:

```python
# Simple alias
UserId = str
Config = dict[str, str | int | bool]

# Complex nested type
JsonValue = dict[str, "JsonValue"] | list["JsonValue"] | str | int | float | bool | None

def load_config() -> Config:
    return {"host": "localhost", "port": 8080}
```

**Note**: Python 3.12 introduces `type` statement for better alias support.

### When from **future** import annotations is Needed

Use `from __future__ import annotations` when you encounter:

**Forward references** (class referencing itself):

```python
from __future__ import annotations

class Node:
    def __init__(self, value: int, parent: Node | None = None):
        self.value = value
        self.parent = parent
```

**Circular type imports**:

```python
# a.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from b import B

class A:
    def method(self) -> B:
        ...
```

**Complex recursive types**:

```python
from __future__ import annotations

JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
```

### Interfaces: ABC vs Protocol

✅ **PREFERRED** - Use ABC for interfaces:

```python
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get(self, id: str) -> User | None:
        """Get user by ID."""

    @abstractmethod
    def save(self, user: User) -> None:
        """Save user."""
```

🟡 **VALID** - Use Protocol only for structural typing:

```python
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

# Any object with draw() method matches
def render(obj: Drawable) -> None:
    obj.draw()
```

**Dignified Python prefers ABC** because it makes inheritance and intent explicit.

## Complete Examples

### Builder Pattern with Self

```python
from typing import Self

class QueryBuilder:
    """SQL query builder with fluent interface."""

    def __init__(self) -> None:
        self._select: list[str] = ["*"]
        self._from: str | None = None
        self._where: list[str] = []
        self._limit: int | None = None

    def select(self, *columns: str) -> Self:
        """Specify columns to select."""
        self._select = list(columns)
        return self

    def from_table(self, table: str) -> Self:
        """Specify table to query."""
        self._from = table
        return self

    def where(self, condition: str) -> Self:
        """Add WHERE condition."""
        self._where.append(condition)
        return self

    def limit(self, n: int) -> Self:
        """Set LIMIT."""
        self._limit = n
        return self

    def build(self) -> str:
        """Build final SQL query."""
        if not self._from:
            raise ValueError("FROM table not specified")

        parts = [f"SELECT {', '.join(self._select)}"]
        parts.append(f"FROM {self._from}")

        if self._where:
            parts.append(f"WHERE {' AND '.join(self._where)}")

        if self._limit:
            parts.append(f"LIMIT {self._limit}")

        return " ".join(parts)

# Usage with type-safe method chaining
query = (
    QueryBuilder()
    .select("id", "name", "email")
    .from_table("users")
    .where("active = true")
    .where("age > 18")
    .limit(10)
    .build()
)
```

### Factory Methods with Self

```python
from typing import Self
from pathlib import Path
import json

class Config:
    """Application configuration with multiple factory methods."""

    def __init__(self, data: dict[str, str | int]) -> None:
        self.data = data

    @classmethod
    def from_json(cls, path: Path) -> Self:
        """Load configuration from JSON file."""
        if not path.exists():
            raise FileNotFoundError(f"Config not found: {path}")

        with path.open(encoding="utf-8") as f:
            data = json.load(f)
        return cls(data)

    @classmethod
    def from_env(cls) -> Self:
        """Load configuration from environment variables."""
        import os
        data = {
            k.lower(): v
            for k, v in os.environ.items()
            if k.startswith("APP_")
        }
        return cls(data)

    @classmethod
    def default(cls) -> Self:
        """Create default configuration."""
        return cls({"host": "localhost", "port": 8080})

    def with_override(self, key: str, value: str | int) -> Self:
        """Return new config with overridden value."""
        new_data = self.data.copy()
        new_data[key] = value
        return type(self)(new_data)

# All factory methods return correct type
config = Config.from_json(Path("config.json"))
dev_config = config.with_override("debug", True)
```

## Type Checking Rules

### What to Type

✅ **MUST type**:

- All public function parameters (except `self`, `cls`)
- All public function return values
- All class attributes (public and private)
- Module-level constants

🟡 **SHOULD type**:

- Internal function signatures
- Complex local variables

🟢 **MAY skip**:

- Simple local variables where type is obvious (`count = 0`)
- Lambda parameters in short inline lambdas
- Loop variables in short comprehensions

### Running Type Checker

```bash
uv run ty check
```

All code should pass type checking without errors.

### Type Checking Configuration

Configure ty in `pyproject.toml`:

```toml
[tool.ty.environment]
python-version = "3.11"
```

## Common Patterns

### Checking for None

✅ **CORRECT** - Check before use:

```python
def process_user(user: User | None) -> str:
    if user is None:
        return "No user"
    return user.name
```

### Dict.get() with Type Safety

✅ **CORRECT** - Handle None case:

```python
def get_port(config: dict[str, int]) -> int:
    port = config.get("port")
    if port is None:
        return 8080
    return port
```

### List Operations

✅ **CORRECT** - Check before accessing:

```python
def first_or_default(items: list[str], default: str) -> str:
    if not items:
        return default
    return items[0]
```

## Migration from Python 3.10

If upgrading from Python 3.10:

1. **Replace bound TypeVar with Self** for self-returning methods:
   - Old: `T = TypeVar("T", bound="ClassName")`
   - New: `from typing import Self` and use `-> Self`

2. **Enjoy improved error messages** (no code changes needed)

3. **All existing 3.10 syntax continues to work**

## References

- [PEP 673: Self Type](https://peps.python.org/pep-0673/)
- [PEP 646: Variadic Generics](https://peps.python.org/pep-0646/)
- [Python 3.11 What's New](https://docs.python.org/3.11/whatsnew/3.11.html)

```

### versions/python-3.12.md

```markdown
---
---

# Type Annotations - Python 3.12

This document provides complete, canonical type annotation guidance for Python 3.12.

## Overview

Python 3.12 introduces PEP 695, a major syntactic improvement for generic types. The new type
parameter syntax makes generic functions and classes significantly more readable. All syntax from
3.10 and 3.11 continues to work.

**What's new in 3.12:**

- PEP 695 type parameter syntax: `def func[T](x: T) -> T`
- `type` statement for better type aliases
- Cleaner generic class syntax

**Available from 3.11:**

- `Self` type for self-returning methods

**Available from 3.10:**

- Built-in generic types: `list[T]`, `dict[K, V]`, etc.
- Union types with `|` operator
- Optional with `X | None`

**What you need from typing module:**

- `Self` for self-returning methods
- `TypeVar` only for constrained/bounded generics
- `Protocol` for structural typing (rare - prefer ABC)
- `TYPE_CHECKING` for conditional imports
- `Any` (use sparingly)

## Complete Type Annotation Syntax for Python 3.12

### Basic Collection Types

✅ **PREFERRED** - Use built-in generic types:

```python
names: list[str] = []
mapping: dict[str, int] = {}
unique_ids: set[str] = set()
coordinates: tuple[int, int] = (0, 0)
```

❌ **WRONG** - Don't use typing module equivalents:

```python
from typing import List, Dict, Set, Tuple  # Don't do this
names: List[str] = []
```

### Union Types

✅ **PREFERRED** - Use `|` operator:

```python
def process(value: str | int) -> str:
    return str(value)

def find_config(name: str) -> dict[str, str] | dict[str, int]:
    ...

# Multiple unions
def parse(input: str | int | float) -> str:
    return str(input)
```

❌ **WRONG** - Don't use `typing.Union`:

```python
from typing import Union
def process(value: Union[str, int]) -> str:  # Don't do this
    ...
```

### Optional Types

✅ **PREFERRED** - Use `X | None`:

```python
def find_user(id: str) -> User | None:
    """Returns user or None if not found."""
    if id in users:
        return users[id]
    return None
```

❌ **WRONG** - Don't use `typing.Optional`:

```python
from typing import Optional
def find_user(id: str) -> Optional[User]:  # Don't do this
    ...
```

### Self Type for Self-Returning Methods

✅ **PREFERRED** - Use Self for methods that return the instance:

```python
from typing import Self

class Builder:
    def set_name(self, name: str) -> Self:
        self.name = name
        return self

    def set_value(self, value: int) -> Self:
        self.value = value
        return self
```

### Generic Functions with PEP 695 (NEW in 3.12)

✅ **PREFERRED** - Use PEP 695 type parameter syntax:

```python
def first[T](items: list[T]) -> T | None:
    """Return first item or None if empty."""
    if not items:
        return None
    return items[0]

def identity[T](value: T) -> T:
    """Return value unchanged."""
    return value

# Multiple type parameters
def zip_dicts[K, V](keys: list[K], values: list[V]) -> dict[K, V]:
    """Create dict from separate key and value lists."""
    return dict(zip(keys, values))
```

🟡 **VALID** - TypeVar still works:

```python
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T | None:
    if not items:
        return None
    return items[0]
```

**Note**: Prefer PEP 695 syntax for simple generics. TypeVar is still needed for constraints/bounds.

### Generic Classes with PEP 695 (NEW in 3.12)

✅ **PREFERRED** - Use PEP 695 class syntax:

```python
class Stack[T]:
    """A generic stack data structure."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> Self:
        self._items.append(item)
        return self

    def pop(self) -> T | None:
        if not self._items:
            return None
        return self._items.pop()

# Usage
int_stack = Stack[int]()
int_stack.push(42).push(43)
```

🟡 **VALID** - Generic with TypeVar still works:

```python
from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    # ... rest of implementation
```

**Note**: PEP 695 is cleaner - no imports needed, type parameter scope is local to class.

### Type Parameter Bounds

✅ **Use bounds with PEP 695**:

```python
class Comparable:
    def compare(self, other: object) -> int:
        ...

def max_value[T: Comparable](items: list[T]) -> T:
    """Get maximum value from comparable items."""
    return max(items, key=lambda x: x)
```

### Constrained TypeVars (Still Use TypeVar)

✅ **Use TypeVar for specific type constraints**:

```python
from typing import TypeVar

# Constrained to specific types - must use TypeVar
Numeric = TypeVar("Numeric", int, float)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b
```

❌ **WRONG** - PEP 695 doesn't support constraints:

```python
# This doesn't constrain to int|float
def add[Numeric](a: Numeric, b: Numeric) -> Numeric:
    return a + b
```

### Type Aliases with type Statement (NEW in 3.12)

✅ **PREFERRED** - Use `type` statement:

```python
# Simple alias
type UserId = str
type Config = dict[str, str | int | bool]

# Generic type alias
type Result[T] = tuple[T, str | None]

def process(value: str) -> Result[int]:
    try:
        return (int(value), None)
    except ValueError as e:
        return (0, str(e))
```

🟡 **VALID** - Simple assignment still works:

```python
UserId = str  # Still valid
Config = dict[str, str | int | bool]  # Still valid
```

**Note**: `type` statement is more explicit and works better with generics.

### Callable Types

✅ **PREFERRED** - Use `collections.abc.Callable`:

```python
from collections.abc import Callable

# Function that takes int, returns str
processor: Callable[[int], str] = str

# Function with no args, returns None
callback: Callable[[], None] = lambda: None

# Function with multiple args
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i
```

### When from **future** import annotations is Needed

Use `from __future__ import annotations` when you encounter:

**Forward references** (class referencing itself):

```python
from __future__ import annotations

class Node:
    def __init__(self, value: int, parent: Node | None = None):
        self.value = value
        self.parent = parent
```

**Circular type imports**:

```python
# a.py
from __future__ import annotations
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from b import B

class A:
    def method(self) -> B:
        ...
```

**Complex recursive types**:

```python
from __future__ import annotations

type JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
```

### Interfaces: ABC vs Protocol

✅ **PREFERRED** - Use ABC for interfaces:

```python
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get(self, id: str) -> User | None:
        """Get user by ID."""

    @abstractmethod
    def save(self, user: User) -> None:
        """Save user."""
```

🟡 **VALID** - Use Protocol only for structural typing:

```python
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()
```

**Dignified Python prefers ABC** because it makes inheritance and intent explicit.

## Complete Examples

### Generic Stack with PEP 695

```python
from typing import Self

class Stack[T]:
    """Type-safe stack with PEP 695 syntax."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> Self:
        """Push item and return self for chaining."""
        self._items.append(item)
        return self

    def pop(self) -> T | None:
        """Pop item or return None if empty."""
        if not self._items:
            return None
        return self._items.pop()

    def peek(self) -> T | None:
        """Peek at top item without removing."""
        if not self._items:
            return None
        return self._items[-1]

    def is_empty(self) -> bool:
        """Check if stack is empty."""
        return len(self._items) == 0

# Usage
numbers = Stack[int]()
numbers.push(1).push(2).push(3)
top = numbers.pop()  # Type checker knows this is int | None
```

### Generic Repository with PEP 695

```python
from abc import ABC, abstractmethod
from typing import Self

class Repository[T]:
    """Abstract repository with generic type parameter."""

    @abstractmethod
    def get(self, id: str) -> T | None:
        """Get entity by ID."""

    @abstractmethod
    def save(self, entity: T) -> Self:
        """Save entity, return self for chaining."""

    @abstractmethod
    def delete(self, id: str) -> bool:
        """Delete entity, return success."""

    def get_or_fail(self, id: str) -> T:
        """Get entity or raise error."""
        entity = self.get(id)
        if entity is None:
            raise ValueError(f"Entity not found: {id}")
        return entity

class InMemoryRepository[T](Repository[T]):
    """In-memory repository implementation."""

    def __init__(self) -> None:
        self._storage: dict[str, T] = {}

    def get(self, id: str) -> T | None:
        return self._storage.get(id)

    def save(self, entity: T) -> Self:
        # Assume entity has 'id' attribute
        entity_id = str(getattr(entity, "id", id(entity)))
        self._storage[entity_id] = entity
        return self

    def delete(self, id: str) -> bool:
        if id in self._storage:
            del self._storage[id]
            return True
        return False

# Usage
from dataclasses import dataclass

@dataclass
class User:
    id: str
    name: str

repo = InMemoryRepository[User]()
repo.save(User("1", "Alice")).save(User("2", "Bob"))
user = repo.get("1")  # Type: User | None
```

### Type Aliases with type Statement

```python
# Simple aliases
type UserId = str
type ErrorMessage = str

# Complex nested types
type JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None

# Generic type aliases
type Result[T] = tuple[T, ErrorMessage | None]
type AsyncResult[T] = tuple[T | None, ErrorMessage | None]

def parse_int(value: str) -> Result[int]:
    """Parse string to int, return result with optional error."""
    try:
        return (int(value), None)
    except ValueError as e:
        return (0, str(e))

def fetch_user(id: UserId) -> AsyncResult[dict[str, str]]:
    """Fetch user data asynchronously."""
    # Implementation...
    return ({"id": id, "name": "Alice"}, None)
```

### Builder Pattern with Self and PEP 695

```python
from typing import Self

class QueryBuilder[T]:
    """Generic query builder with fluent interface."""

    def __init__(self, result_type: type[T]) -> None:
        self._result_type = result_type
        self._filters: list[str] = []
        self._limit: int | None = None

    def filter(self, condition: str) -> Self:
        """Add filter condition."""
        self._filters.append(condition)
        return self

    def limit(self, n: int) -> Self:
        """Set result limit."""
        self._limit = n
        return self

    def build(self) -> str:
        """Build query string."""
        query = " AND ".join(self._filters)
        if self._limit:
            query += f" LIMIT {self._limit}"
        return query

# Usage
@dataclass
class User:
    name: str
    age: int

builder = QueryBuilder[User](User)
query = (
    builder
    .filter("active = true")
    .filter("age > 18")
    .limit(10)
    .build()
)
```

### Generic Function Utilities

```python
def map_list[T, U](items: list[T], func: Callable[[T], U]) -> list[U]:
    """Map function over list items."""
    from collections.abc import Callable
    return [func(item) for item in items]

def filter_list[T](items: list[T], predicate: Callable[[T], bool]) -> list[T]:
    """Filter list by predicate."""
    from collections.abc import Callable
    return [item for item in items if predicate(item)]

def reduce_list[T, U](
    items: list[T],
    func: Callable[[U, T], U],
    initial: U,
) -> U:
    """Reduce list to single value."""
    from collections.abc import Callable
    result = initial
    for item in items:
        result = func(result, item)
    return result

# Usage
numbers = [1, 2, 3, 4, 5]
doubled = map_list(numbers, lambda x: x * 2)  # list[int]
evens = filter_list(numbers, lambda x: x % 2 == 0)  # list[int]
sum_val = reduce_list(numbers, lambda acc, x: acc + x, 0)  # int
```

## Type Checking Rules

### What to Type

✅ **MUST type**:

- All public function parameters (except `self`, `cls`)
- All public function return values
- All class attributes (public and private)
- Module-level constants

🟡 **SHOULD type**:

- Internal function signatures
- Complex local variables

🟢 **MAY skip**:

- Simple local variables where type is obvious (`count = 0`)
- Lambda parameters in short inline lambdas
- Loop variables in short comprehensions

### Running Type Checker

```bash
uv run ty check
```

All code should pass type checking without errors.

### Type Checking Configuration

Configure ty in `pyproject.toml`:

```toml
[tool.ty.environment]
python-version = "3.12"
```

## Common Patterns

### Checking for None

✅ **CORRECT** - Check before use:

```python
def process_user(user: User | None) -> str:
    if user is None:
        return "No user"
    return user.name
```

### Dict.get() with Type Safety

✅ **CORRECT** - Handle None case:

```python
def get_port(config: dict[str, int]) -> int:
    port = config.get("port")
    if port is None:
        return 8080
    return port
```

### List Operations

✅ **CORRECT** - Check before accessing:

```python
def first_or_default[T](items: list[T], default: T) -> T:
    if not items:
        return default
    return items[0]
```

## When to Use PEP 695 vs TypeVar

**Use PEP 695 for**:

- Simple generic functions (no constraints/bounds)
- Simple generic classes
- Most common generic use cases
- New code

**Still use TypeVar for**:

- Constrained type variables: `TypeVar("T", str, bytes)`
- Bound type variables with complex bounds
- Covariant/contravariant type variables
- Reusing same TypeVar across multiple functions

## Migration from Python 3.11

If upgrading from Python 3.11:

1. **Consider migrating to PEP 695 syntax**:
   - `TypeVar` + `def func(x: T) -> T` → `def func[T](x: T) -> T`
   - `Generic[T]` + `class C(Generic[T])` → `class C[T]`

2. **Consider using `type` statement for aliases**:
   - `Config = dict[str, str]` → `type Config = dict[str, str]`

3. **Keep TypeVar for constraints**:
   - `TypeVar` with constraints still needed

4. **All existing 3.11 syntax continues to work**:
   - `Self` type still preferred
   - Union with `|` still preferred

## References

- [PEP 695: Type Parameter Syntax](https://peps.python.org/pep-0695/)
- [Python 3.12 What's New - Type Hints](https://docs.python.org/3.12/whatsnew/3.12.html)

```

### versions/python-3.13.md

```markdown
---
---

# Type Annotations - Python 3.13

This document provides complete, canonical type annotation guidance for Python 3.13. Python 3.13
implements PEP 649 (Deferred Evaluation of Annotations), fundamentally changing how annotations are
evaluated.

## Overview

**The key change: forward references and circular imports work naturally without
`from __future__ import annotations`.**

All type features from previous versions (3.10-3.12) continue to work.

**What's new in 3.13:**

- PEP 649 deferred annotation evaluation
- Forward references work naturally (no quotes, no `from __future__`)
- Circular imports no longer cause annotation errors
- **DO NOT use `from __future__ import annotations`**

**Available from 3.12:**

- PEP 695 type parameter syntax: `def func[T](x: T) -> T`
- `type` statement for better type aliases

**Available from 3.11:**

- `Self` type for self-returning methods

## Universal Philosophy

**Code Clarity:**

- Types serve as inline documentation
- Make function contracts explicit
- Reduce cognitive load when reading code
- Help understand data flow without tracing through implementation

**IDE Support:**

- Enable autocomplete and intelligent suggestions
- Catch typos and attribute errors before runtime
- Support refactoring tools (rename, move, extract)
- Provide jump-to-definition for typed objects

**Bug Prevention:**

- Catch type mismatches during static analysis
- Prevent None-related errors with explicit optional types
- Document expected input/output without running code
- Enable early detection of API contract violations

## Consistency Rules

**All public APIs:**

- 🔴 MUST: Type all function parameters (except `self` and `cls`)
- 🔴 MUST: Type all function return values
- 🔴 MUST: Type all class attributes
- 🟡 SHOULD: Type module-level constants

**Internal code:**

- 🟡 SHOULD: Type function signatures where helpful for clarity
- 🟢 MAY: Type complex local variables where type isn't obvious
- 🟢 MAY: Omit types for obvious cases (e.g., `count = 0`)

## Basic Collection Types

✅ **PREFERRED** - Use built-in generic types:

```python
names: list[str] = []
mapping: dict[str, int] = {}
unique_ids: set[str] = set()
coordinates: tuple[int, int] = (0, 0)
```

❌ **WRONG** - Don't use typing module equivalents:

```python
from typing import List, Dict, Set, Tuple  # Don't do this
names: List[str] = []
```

**Why**: Built-in types are more concise, don't require imports, and are the modern Python standard
(available since 3.10).

## Union Types

✅ **PREFERRED** - Use `|` operator:

```python
def process(value: str | int) -> str:
    return str(value)

def find_config(name: str) -> dict[str, str] | dict[str, int]:
    ...

# Multiple unions
def parse(input: str | int | float) -> str:
    return str(input)
```

❌ **WRONG** - Don't use `typing.Union`:

```python
from typing import Union
def process(value: Union[str, int]) -> str:  # Don't do this
    ...
```

## Optional Types

✅ **PREFERRED** - Use `X | None`:

```python
def find_user(id: str) -> User | None:
    """Returns user or None if not found."""
    if id in users:
        return users[id]
    return None
```

❌ **WRONG** - Don't use `typing.Optional`:

```python
from typing import Optional
def find_user(id: str) -> Optional[User]:  # Don't do this
    ...
```

## Callable Types

✅ **PREFERRED** - Use `collections.abc.Callable`:

```python
from collections.abc import Callable

# Function that takes int, returns str
processor: Callable[[int], str] = str

# Function with no args, returns None
callback: Callable[[], None] = lambda: None

# Function with multiple args
validator: Callable[[str, int], bool] = lambda s, i: len(s) > i
```

## Interfaces: ABC vs Protocol

✅ **PREFERRED** - Use ABC for interfaces:

```python
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def get(self, id: str) -> User | None:
        """Get user by ID."""

    @abstractmethod
    def save(self, user: User) -> None:
        """Save user."""
```

🟡 **VALID** - Use Protocol only for structural typing:

```python
from typing import Protocol

class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:
    obj.draw()
```

**Dignified Python prefers ABC** because it makes inheritance and intent explicit.

## Self Type for Self-Returning Methods (3.11+)

✅ **PREFERRED** - Use Self for methods that return the instance:

```python
from typing import Self

class Builder:
    def set_name(self, name: str) -> Self:
        self.name = name
        return self

    def set_value(self, value: int) -> Self:
        self.value = value
        return self
```

## Generic Functions with PEP 695 (3.12+)

✅ **PREFERRED** - Use PEP 695 type parameter syntax:

```python
def first[T](items: list[T]) -> T | None:
    """Return first item or None if empty."""
    if not items:
        return None
    return items[0]

def identity[T](value: T) -> T:
    """Return value unchanged."""
    return value

# Multiple type parameters
def zip_dicts[K, V](keys: list[K], values: list[V]) -> dict[K, V]:
    """Create dict from separate key and value lists."""
    return dict(zip(keys, values))
```

🟡 **VALID** - TypeVar still works:

```python
from typing import TypeVar

T = TypeVar("T")

def first(items: list[T]) -> T | None:
    if not items:
        return None
    return items[0]
```

**Note**: Prefer PEP 695 syntax for simple generics. TypeVar is still needed for constraints/bounds.

## Generic Classes with PEP 695 (3.12+)

✅ **PREFERRED** - Use PEP 695 class syntax:

```python
class Stack[T]:
    """A generic stack data structure."""

    def __init__(self) -> None:
        self._items: list[T] = []

    def push(self, item: T) -> Self:
        self._items.append(item)
        return self

    def pop(self) -> T | None:
        if not self._items:
            return None
        return self._items.pop()

# Usage
int_stack = Stack[int]()
int_stack.push(42).push(43)
```

🟡 **VALID** - Generic with TypeVar still works:

```python
from typing import Generic, TypeVar

T = TypeVar("T")

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    # ... rest of implementation
```

**Note**: PEP 695 is cleaner - no imports needed, type parameter scope is local to class.

## Type Parameter Bounds (3.12+)

✅ **Use bounds with PEP 695**:

```python
class Comparable:
    def compare(self, other: object) -> int:
        ...

def max_value[T: Comparable](items: list[T]) -> T:
    """Get maximum value from comparable items."""
    return max(items, key=lambda x: x)
```

## Constrained TypeVars (Still Use TypeVar)

✅ **Use TypeVar for specific type constraints**:

```python
from typing import TypeVar

# Constrained to specific types - must use TypeVar
Numeric = TypeVar("Numeric", int, float)

def add(a: Numeric, b: Numeric) -> Numeric:
    return a + b
```

❌ **WRONG** - PEP 695 doesn't support constraints:

```python
# This doesn't constrain to int|float
def add[Numeric](a: Numeric, b: Numeric) -> Numeric:
    return a + b
```

## Type Aliases with type Statement (3.12+)

✅ **PREFERRED** - Use `type` statement:

```python
# Simple alias
type UserId = str
type Config = dict[str, str | int | bool]

# Generic type alias
type Result[T] = tuple[T, str | None]

def process(value: str) -> Result[int]:
    try:
        return (int(value), None)
    except ValueError as e:
        return (0, str(e))
```

🟡 **VALID** - Simple assignment still works:

```python
UserId = str  # Still valid
Config = dict[str, str | int | bool]  # Still valid
```

**Note**: `type` statement is more explicit and works better with generics.

## Forward References and Circular Imports (NEW in 3.13)

✅ **CORRECT** - Just works naturally with PEP 649:

```python
# Forward reference - no quotes needed!
class Node:
    def __init__(self, value: int, parent: Node | None = None):
        self.value = value
        self.parent = parent

# Circular imports - just works!
# a.py
from b import B

class A:
    def method(self) -> B:
        ...

# b.py
from a import A

class B:
    def method(self) -> A:
        ...

# Recursive types - no future needed!
type JsonValue = dict[str, JsonValue] | list[JsonValue] | str | int | float | bool | None
```

❌ **WRONG** - Don't use `from __future__ import annotations`:

```python
from __future__ import annotations  # DON'T DO THIS in Python 3.13

class Node:
    def __init__(self, value: int, parent: Node | None = None):
        ...
```

**Why avoid `from __future__ import annotations` in 3.13:**

- Unnecessary - PEP 649 provides better default behavior
- Can cause confusion
- Masks the native 3.13 deferred evaluation
- Prevents you from leveraging improvements

## Complete Examples

### Tree Structure with Natural Forward References

```python
from typing import Self
from collections.abc import Callable

class Node[T]:
    """Tree node - forward reference works naturally in 3.13!"""

    def __init__(
        self,
        value: T,
        parent: Node[T] | None = None,  # Forward ref, no quotes!
        children: list[Node[T]] | None = None,  # Forward ref, no quotes!
    ) -> None:
        self.value = value
        self.parent = parent
        self.children = children or []

    def add_child(self, child: Node[T]) -> Self:
        """Add child and return self for chaining."""
        self.children.append(child)
        child.parent = self
        return self

    def find(self, predicate: Callable[[T], bool]) -> Node[T] | None:
        """Find first node matching predicate."""
        if predicate(self.value):
            return self

        for child in self.children:
            result = child.find(predicate)
            if result:
                return result

        return None

# Usage - all type-safe with no __future__ import!
root = Node[int](1)
root.add_child(Node[int](2)).add_child(Node[int](3))
```

### Generic Repository with PEP 695

```python
from abc import ABC, abstractmethod
from typing import Self

class Entity[T]:
    """Base class for entities."""

    def __init__(self, id: T) -> None:
        self.id = id

class Repository[T](ABC):
    """Generic repository interface."""

    @abstractmethod
    def get(self, id: str) -> T | None:
        """Get entity by ID."""

    @abstractmethod
    def save(self, entity: T) -> None:
        """Save entity."""

    @abstractmethod
    def delete(self, id: str) -> bool:
        """Delete entity, return True if deleted."""

class User(Entity[str]):
    def __init__(self, id: str, name: str) -> None:
        super().__init__(id)
        self.name = name

class UserRepository(Repository[User]):
    def __init__(self) -> None:
        self._users: dict[str, User] = {}

    def get(self, id: str) -> User | None:
        if id not in self._users:
            return None
        return self._users[id]

    def save(self, entity: User) -> None:
        self._users[entity.id] = entity

    def delete(self, id: str) -> bool:
        if id not in self._users:
            return False
        del self._users[id]
        return True
```

## General Best Practices

**Prefer specificity:**

```python
# ✅ GOOD - Specific
def get_config() -> dict[str, str | int]:
    ...

# ❌ WRONG - Too vague
def get_config() -> dict:
    ...
```

**Use Union sparingly:**

```python
# ✅ GOOD - Union only when necessary
def process(value: str | int) -> str:
    ...

# ❌ WRONG - Too permissive
def process(value: str | int | list | dict) -> str | None | list:
    ...
```

**Be explicit with None:**

```python
# ✅ GOOD - Explicit optional
def find_user(id: str) -> User | None:
    ...

# ❌ WRONG - Implicit None return
def find_user(id: str) -> User:
    return None  # Type checker error!
```

**Avoid Any when possible:**

```python
# ✅ GOOD - Specific type
def serialize(obj: User | Config) -> str:
    ...

# ❌ WRONG - Defeats purpose of types
from typing import Any
def serialize(obj: Any) -> str:
    ...
```

## When to Use Types

**Always type:**

- Public function signatures (parameters + return)
- Class attributes (including private ones)
- Function parameters that cross module boundaries
- Return values that aren't immediately obvious

**Type when helpful:**

- Complex local variables
- Closures and nested functions
- Lambda expressions used as callbacks

**Can skip:**

- Obvious cases: `count = 0`, `name = "example"`
- Trivial private helpers
- Test fixture setup code (if types add no clarity)

## Type Checking with ty

Dignified Python uses ty for static type checking:

```bash
# Check all files
ty check

# Check specific file
ty check src/mymodule.py

# Check with specific Python version
ty check --python-version 3.13
```

**Configuration** (in `pyproject.toml`):

```toml
[tool.ty.environment]
python-version = "3.13"
```

## Anti-Patterns

**❌ Don't ignore type errors with `# type: ignore`**

```python
# ❌ WRONG - Hiding type error
result = unsafe_function()  # type: ignore

# ✅ CORRECT - Fix the type error
result: Expected = cast(Expected, unsafe_function())
```

**❌ Don't use bare Exception in type hints**

```python
# ❌ WRONG - No value from typing exception
def risky() -> str | Exception:
    ...

# ✅ CORRECT - Let exceptions bubble
def risky() -> str:
    ...  # Raises ValueError on error
```

**❌ Don't over-type simple cases**

```python
# ❌ WRONG - Obvious from context
def add_numbers(a: int, b: int) -> int:
    result: int = a + b  # Unnecessary type annotation
    return result

# ✅ CORRECT - Type only signature
def add_numbers(a: int, b: int) -> int:
    result = a + b  # Type is obvious
    return result
```

## Migration from 3.10/3.11

If migrating from Python 3.10/3.11:

1. **Remove `from __future__ import annotations`** - No longer needed
2. **Consider upgrading to PEP 695 syntax** - Cleaner generics
3. **Use `type` statement for aliases** - More explicit than assignment
4. **Remove quoted forward references** - They work naturally now

```python
# Python 3.10/3.11
from __future__ import annotations
from typing import TypeVar, Generic

T = TypeVar("T")

class Node(Generic[T]):
    def __init__(self, value: T, parent: "Node[T] | None" = None):
        ...

# Python 3.13
from typing import Self

class Node[T]:
    def __init__(self, value: T, parent: Node[T] | None = None):
        ...
```

## What typing imports are still needed?

**Very rare:**

- `TypeVar` - Only for constrained/bounded type variables
- `Any` - Use sparingly when type truly unknown
- `Protocol` - Structural typing (prefer ABC)
- `TYPE_CHECKING` - Conditional imports to avoid circular dependencies

**Never needed:**

- `List`, `Dict`, `Set`, `Tuple` - Use built-in types
- `Union` - Use `|` operator
- `Optional` - Use `X | None`
- `Generic` - Use PEP 695 class syntax

```

### references/exception-handling.md

```markdown
---
description:
  Detailed exception handling patterns including B904 chaining, third-party API compatibility, and
  anti-patterns.
---

# Exception Handling Reference

**Read when**: Writing try/except blocks, wrapping third-party APIs, seeing `from e` or `from None`

---

## When Exceptions ARE Acceptable

Exceptions are ONLY acceptable at:

1. **Error boundaries** (CLI/API level)
2. **Third-party API compatibility** (when no alternative exists)
3. **Adding context before re-raising**

### 1. Error Boundaries

```python
# ACCEPTABLE: CLI command error boundary
@click.command("create")
@click.pass_obj
def create(ctx: ErkContext, name: str) -> None:
    """Create a worktree."""
    try:
        create_worktree(ctx, name)
    except subprocess.CalledProcessError as e:
        click.echo(f"Error: Git command failed: {e.stderr}", err=True)
        raise SystemExit(1)
```

### 2. Third-Party API Compatibility

```python
# ACCEPTABLE: Third-party API forces exception handling
def _get_bigquery_sample(sql_client, table_name):
    """
    BigQuery's TABLESAMPLE doesn't work on views.
    There's no reliable way to determine a priori whether
    a table supports TABLESAMPLE.
    """
    try:
        return sql_client.run_query(f"SELECT * FROM {table_name} TABLESAMPLE...")
    except Exception:
        return sql_client.run_query(f"SELECT * FROM {table_name} ORDER BY RAND()...")
```

> **The test for "no alternative exists"**: Can you validate or check the condition BEFORE calling
> the API? If yes (even using a different function/method), use LBYL. The exception only applies
> when the API provides NO way to determine success a priori—you literally must attempt the
> operation to know if it will work.

### What Does NOT Qualify as Third-Party API Compatibility

Standard library functions with known LBYL alternatives do NOT qualify:

```python
# WRONG: int() has LBYL alternative (str.isdigit)
try:
    port = int(user_input)
except ValueError:
    port = 80

# CORRECT: Check before calling
if user_input.lstrip('-+').isdigit():
    port = int(user_input)
else:
    port = 80

# WRONG: datetime.fromisoformat() can be validated first
try:
    dt = datetime.fromisoformat(timestamp_str)
except ValueError:
    dt = None

# CORRECT: Validate format before parsing
def _is_iso_format(s: str) -> bool:
    return len(s) >= 10 and s[4] == "-" and s[7] == "-"

if _is_iso_format(timestamp_str):
    dt = datetime.fromisoformat(timestamp_str)
else:
    dt = None
```

### 3. Adding Context Before Re-raising

```python
# ACCEPTABLE: Adding context before re-raising
try:
    process_file(config_file)
except yaml.YAMLError as e:
    raise ValueError(f"Failed to parse config file {config_file}: {e}") from e
```

---

## Exception Chaining (B904 Lint Compliance)

**Ruff rule B904** requires explicit exception chaining when raising inside an `except` block. This
prevents losing the original traceback.

```python
# CORRECT: Chain to preserve context
try:
    parse_config(path)
except ValueError as e:
    click.echo(json.dumps({"success": False, "error": str(e)}))
    raise SystemExit(1) from e  # Preserves traceback

# CORRECT: Explicitly break chain when intentional
try:
    fetch_from_cache(key)
except KeyError:
    # Original exception is not relevant to caller
    raise ValueError(f"Unknown key: {key}") from None

# WRONG: Missing exception chain (B904 violation)
try:
    parse_config(path)
except ValueError:
    raise SystemExit(1)  # Lint error: missing 'from e' or 'from None'

# CORRECT: CLI error boundary with JSON output
try:
    result = some_operation()
except RuntimeError as e:
    click.echo(json.dumps({"success": False, "error": str(e)}))
    raise SystemExit(0) from None  # Exception is in JSON, traceback irrelevant to CLI user
```

**When to use each:**

- `from e` - Preserve original exception for debugging
- `from None` - Intentionally suppress original (e.g., transforming exception type, CLI JSON output)

---

## Exception Anti-Patterns

**Never swallow exceptions silently**

Even at error boundaries, you must at least log/warn so issues can be diagnosed:

```python
# WRONG: Silent exception swallowing
try:
    risky_operation()
except:
    pass

# WRONG: Silent swallowing even at error boundary
try:
    optional_feature()
except Exception:
    pass  # Silent - impossible to diagnose issues

# CORRECT: Let exceptions bubble up (default)
risky_operation()

# CORRECT: At error boundaries, log the exception
try:
    optional_feature()
except Exception as e:
    logging.warning("Optional feature failed: %s", e)  # Diagnosable
```

**Never use silent fallback behavior**

```python
# WRONG: Silent fallback masks failure
def process_text(text: str) -> dict:
    try:
        return llm_client.process(text)
    except Exception:
        return regex_parse_fallback(text)

# CORRECT: Let error bubble to boundary
def process_text(text: str) -> dict:
    return llm_client.process(text)
```

```

### references/interfaces.md

```markdown
---
description:
  ABC vs Protocol decision guide, dependency injection patterns, and complete DI examples.
---

# Interface Design Reference

**Read when**: Creating ABC/Protocol classes, writing @abstractmethod, designing gateway interfaces

---

## ABC vs Protocol: Choosing the Right Interface

**ABCs (nominal typing)** and **Protocols (structural typing)** serve different purposes. Choose
based on ownership and coupling needs.

| Use Case                                  | Recommended | Why                                                  |
| ----------------------------------------- | ----------- | ---------------------------------------------------- |
| Internal interfaces you control           | ABC         | Explicit enforcement, runtime validation, code reuse |
| Third-party library boundaries            | Protocol    | No inheritance required, loose coupling              |
| Plugin systems with isinstance checks     | ABC         | Reliable runtime type validation                     |
| Minimal interface contracts (1-2 methods) | Protocol    | Less boilerplate, focused contracts                  |

**Default for erk internal code: ABC. Default for external library facades: Protocol.**

---

## ABC Interface Pattern

```python
# CORRECT: Use ABC for interfaces
from abc import ABC, abstractmethod

class Repository(ABC):
    @abstractmethod
    def save(self, entity: Entity) -> None:
        """Save entity to storage."""
        ...

    @abstractmethod
    def load(self, id: str) -> Entity:
        """Load entity by ID."""
        ...

class PostgresRepository(Repository):
    def save(self, entity: Entity) -> None:
        # Implementation
        pass

    def load(self, id: str) -> Entity:
        # Implementation
        pass
```

---

## Benefits of ABC (Internal Interfaces)

1. **Explicit inheritance** - Clear class hierarchy, explicit opt-in
2. **Runtime validation** - Errors at instantiation if abstract methods missing
3. **Code reuse** - Can include concrete methods and shared logic
4. **Reliable isinstance()** - Full signature checking at runtime

---

## Benefits of Protocol (External Boundaries)

1. **No inheritance required** - Works with code you don't control
2. **Loose coupling** - Implementations don't know about the protocol
3. **Minimal contracts** - Define only the methods you need
4. **Duck typing** - Aligns with Python's philosophy

---

## Complete DI Example

```python
from abc import ABC, abstractmethod
from dataclasses import dataclass

# Define the interface
class DataStore(ABC):
    @abstractmethod
    def get(self, key: str) -> str | None:
        """Retrieve value by key."""
        ...

    @abstractmethod
    def set(self, key: str, value: str) -> None:
        """Store value with key."""
        ...

# Real implementation
class RedisStore(DataStore):
    def get(self, key: str) -> str | None:
        return self.client.get(key)

    def set(self, key: str, value: str) -> None:
        self.client.set(key, value)

# Fake for testing
class FakeStore(DataStore):
    def __init__(self) -> None:
        self._data: dict[str, str] = {}

    def get(self, key: str) -> str | None:
        if key not in self._data:
            return None
        return self._data[key]

    def set(self, key: str, value: str) -> None:
        self._data[key] = value

# Business logic accepts interface
@dataclass
class Service:
    store: DataStore  # Depends on abstraction

    def process(self, item: str) -> None:
        cached = self.store.get(item)
        if cached is None:
            result = expensive_computation(item)
            self.store.set(item, result)
        else:
            result = cached
        use_result(result)
```

---

## When to Use Protocol

**Protocols excel at defining interfaces for code you don't control:**

```python
# CORRECT: Protocol for third-party library facade
from typing import Protocol

class HttpClient(Protocol):
    """Interface for HTTP operations - decouples from requests/httpx/aiohttp."""
    def get(self, url: str) -> Response: ...
    def post(self, url: str, data: dict) -> Response: ...

# Any HTTP library that has these methods works - no inheritance needed
def fetch_data(client: HttpClient, endpoint: str) -> dict:
    response = client.get(endpoint)
    return response.json()
```

**Protocols are also appropriate for minimal, focused interfaces:**

```python
# CORRECT: Protocol for structural typing with minimal interface
from typing import Protocol

class Closeable(Protocol):
    def close(self) -> None: ...

def cleanup_resources(resources: list[Closeable]) -> None:
    for r in resources:
        r.close()
```

---

## Protocol Limitations

1. **No runtime validation** - `@runtime_checkable` only checks method existence, not signatures
2. **No code reuse** - Protocols shouldn't have method implementations
3. **Weaker isinstance() checks** - ABCs provide more reliable runtime type checking

---

## Decision Checklist

Before defining an interface (ABC or Protocol):

- [ ] Do I own all implementations? -> Prefer ABC
- [ ] Am I wrapping a third-party library? -> Prefer Protocol
- [ ] Do I need runtime isinstance() validation? -> Use ABC
- [ ] Is this a minimal interface (1-2 methods)? -> Protocol may be simpler
- [ ] Do I need shared method implementations? -> Use ABC

**Default for erk internal code: ABC. Default for external library facades: Protocol.**

```

### references/typing-advanced.md

```markdown
---
description:
  Advanced typing patterns including cast() with assertions, Literal types for programmatic strings.
---

# Advanced Typing Reference

**Read when**: Using typing.cast(), creating Literal type aliases, narrowing types

---

## Using `typing.cast()`

### Core Rule

**ALWAYS verify `cast()` with a runtime assertion, unless there's a documented reason not to.**

`typing.cast()` is a compile-time only construct—it tells the type checker to trust you but performs
no runtime verification. If your assumption is wrong, you'll get silent misbehavior instead of a
clear error.

### Required Pattern

```python
from collections.abc import MutableMapping
from typing import Any, cast

# CORRECT: Runtime assertion before cast
assert isinstance(doc, MutableMapping), f"Expected MutableMapping, got {type(doc)}"
cast(dict[str, Any], doc)["key"] = value

# CORRECT: Alternative with hasattr for duck typing
assert hasattr(obj, '__setitem__'), f"Expected subscriptable, got {type(obj)}"
cast(dict[str, Any], obj)["key"] = value
```

### Anti-Pattern

```python
# WRONG: Cast without runtime verification
cast(dict[str, Any], doc)["key"] = value  # If doc isn't a dict-like, silent failure
```

### When to Skip Runtime Verification

**Default: Always add the assertion when cost is trivial (O(1) checks like `in`, `isinstance`).**

Skip the assertion only in these narrow cases:

1. **Immediately after a type guard**: The check was just performed and would be redundant

   ```python
   if isinstance(value, str):
       # No assertion needed - we just checked
       result = cast(str, value).upper()
   ```

2. **Performance-critical hot path**: Add a comment explaining the measured overhead
   ```python
   # Skip assertion: called 10M times/sec, isinstance adds 15% overhead
   # Type invariant maintained by _validate_input() at entry point
   cast(int, cached_value)
   ```

**What is NOT a valid reason to skip:**

- "Click validates the choice set" - Add assertion anyway; cost is trivial
- "The library guarantees the type" - Add assertion anyway; defense in depth
- "It's obvious from context" - Add assertion anyway; future readers benefit

### Why This Matters

- **Silent bugs are worse than loud bugs**: An assertion failure gives you a stack trace and clear
  error message
- **Documentation**: The assertion documents your assumption for future readers
- **Defense in depth**: Third-party libraries can change behavior between versions

---

## Programmatically Significant Strings

**Use `Literal` types for strings that have programmatic meaning.**

When strings represent a fixed set of valid values (error codes, status values, command types),
model them in the type system using `Literal`.

### Why This Matters

1. **Type safety** - Typos caught at type-check time, not runtime
2. **IDE support** - Autocomplete shows valid options
3. **Documentation** - Valid values are explicit in the code
4. **Refactoring** - Rename operations work correctly

### Naming Convention

**Use kebab-case for all internal Literal string values:**

```python
# CORRECT: kebab-case for internal values
IssueCode = Literal["orphan-state", "orphan-dir", "missing-branch"]
ErrorType = Literal["not-found", "invalid-format", "timeout-exceeded"]
```

**Exception: When modeling external systems, match the external API's convention:**

```python
# CORRECT: Match GitHub API's UPPER_CASE
PRState = Literal["OPEN", "MERGED", "CLOSED"]

# CORRECT: Match GitHub Actions API's lowercase
WorkflowStatus = Literal["completed", "in_progress", "queued"]
```

The rule is: kebab-case by default, external convention when modeling external APIs.

### Pattern

```python
from dataclasses import dataclass
from typing import Literal

# CORRECT: Define a type alias for the valid values
IssueCode = Literal["orphan-state", "orphan-dir", "missing-branch"]

@dataclass(frozen=True)
class Issue:
    code: IssueCode
    message: str

def check_state() -> list[Issue]:
    issues: list[Issue] = []
    if problem_detected:
        issues.append(Issue(code="orphan-state", message="description"))  # Type-checked!
    return issues

# WRONG: Bare strings without type constraint
def check_state() -> list[tuple[str, str]]:
    issues: list[tuple[str, str]] = []
    issues.append(("orphen-state", "desc"))  # Typo goes unnoticed!
    return issues
```

### When to Use Literal

- Error/issue codes
- Status values (pending, complete, failed)
- Command types or action names
- Configuration keys with fixed valid values
- Any string that is compared programmatically

### Decision Checklist

Before using a bare `str` type, ask:

- Is this string compared with `==` or `in` anywhere?
- Is there a fixed set of valid values?
- Would a typo in this string cause a bug?

If any answer is "yes", use `Literal` instead.

```

### references/module-design.md

```markdown
---
description: Import-time side effects, @cache for deferred computation, module-level code patterns.
---

# Module Design Reference

**Read when**: Creating new modules, adding module-level code, using @cache decorator

---

## Import-Time Side Effects

### Core Rule

**Avoid computation and side effects at import time. Defer to function calls.**

Module-level code runs when the module is imported. Side effects at import time cause:

1. **Slower startup** - Every import triggers computation
2. **Test brittleness** - Hard to mock/control behavior
3. **Circular import issues** - Dependencies evaluated too early
4. **Unpredictable order** - Import order affects behavior

---

## Common Anti-Patterns

```python
# WRONG: Path computed at import time
SESSION_ID_FILE = Path(".erk/scratch/current-session-id")

def get_session_id() -> str | None:
    if SESSION_ID_FILE.exists():
        return SESSION_ID_FILE.read_text(encoding="utf-8")
    return None

# WRONG: Config loaded at import time
CONFIG = load_config()  # I/O at import!

# WRONG: Connection established at import time
DB_CLIENT = DatabaseClient(os.environ["DB_URL"])  # Side effect at import!
```

---

## Correct Patterns

**Use `@cache` for deferred computation:**

```python
from functools import cache

# CORRECT: Defer computation until first call
@cache
def _session_id_file_path() -> Path:
    """Return path to session ID file (cached after first call)."""
    return Path(".erk/scratch/current-session-id")

def get_session_id() -> str | None:
    session_file = _session_id_file_path()
    if session_file.exists():
        return session_file.read_text(encoding="utf-8")
    return None
```

**Use functions for resources:**

```python
# CORRECT: Defer resource creation to function call
@cache
def get_config() -> Config:
    """Load config on first call, cache result."""
    return load_config()

@cache
def get_db_client() -> DatabaseClient:
    """Create database client on first call."""
    return DatabaseClient(os.environ["DB_URL"])
```

---

## When Module-Level Constants ARE Acceptable

Simple, static values that don't involve computation or I/O:

```python
# ACCEPTABLE: Static constants
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3
SUPPORTED_FORMATS = frozenset({"json", "yaml", "toml"})
```

---

## Inline Import Patterns

### Core Rules

1. **Default: ALWAYS place imports at module level**
2. **Use absolute imports only** (no relative imports)
3. **Inline imports only for specific exceptions** (see below)

### Legitimate Inline Import Patterns

#### 1. Circular Import Prevention

```python
# commands/sync.py
def register_commands(cli_group):
    """Register commands with CLI group (avoids circular import)."""
    from myapp.cli import sync_command  # Breaks circular dependency
    cli_group.add_command(sync_command)
```

**When to use:**

- CLI command registration
- Plugin systems with bidirectional dependencies
- Lazy loading to break import cycles

#### 2. Conditional Feature Imports

```python
def process_data(data: dict, dry_run: bool = False) -> None:
    if dry_run:
        # Inline import: Only needed for dry-run mode
        from myapp.dry_run import NoopProcessor
        processor = NoopProcessor()
    else:
        processor = RealProcessor()
    processor.execute(data)
```

**When to use:**

- Debug/verbose mode utilities
- Dry-run mode wrappers
- Optional feature modules
- Platform-specific implementations

#### 3. TYPE_CHECKING Imports

```python
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from myapp.models import User  # Only for type hints

def process_user(user: "User") -> None:
    ...
```

**When to use:**

- Avoiding circular dependencies in type hints
- Forward declarations

#### 4. Startup Time Optimization (Rare)

Some packages have genuinely heavy import costs (pyspark, jupyter ecosystem, large ML frameworks).
Deferring these imports can improve CLI startup time.

**However, apply "innocent until proven guilty":**

- Default to module-level imports
- Only defer imports when you have MEASURED evidence of startup impact
- Document the measured cost in a comment

```python
# ACCEPTABLE: Measured heavy import (adds 800ms to startup)
def run_spark_job(config: SparkConfig) -> None:
    from pyspark.sql import SparkSession  # Heavy: 800ms import time
    session = SparkSession.builder.getOrCreate()
    ...

# WRONG: Speculative deferral without measurement
def check_staleness(project_dir: Path) -> None:
    # Inline imports to avoid import-time side effects  <- WRONG: no evidence
    from myapp.staleness import get_version
    ...
```

**When NOT to defer:**

- Standard library modules
- Lightweight internal modules
- Modules you haven't measured
- "Just in case" optimization

---

## Decision Checklist

Before writing module-level code:

- [ ] Does this involve any computation (even `Path()` construction)?
- [ ] Does this involve I/O (file, network, environment)?
- [ ] Could this fail or raise exceptions?
- [ ] Would tests need to mock this value?

If any answer is "yes", wrap in a `@cache`-decorated function instead.

Before inline imports:

- [ ] Is this to break a circular dependency?
- [ ] Is this for TYPE_CHECKING?
- [ ] Is this for conditional features?
- [ ] If for startup time: Have I MEASURED the import cost?
- [ ] If for startup time: Is the cost significant (>100ms)?
- [ ] If for startup time: Have I documented the measured cost in a comment?
- [ ] Have I documented why the inline import is needed?

**Default: Module-level imports**

```

### references/api-design.md

```markdown
---
description:
  Default parameter dangers, keyword-only arguments, ThreadPoolExecutor patterns, speculative test
  infrastructure.
---

# API Design Reference

**Read when**: Adding default parameters, functions with 5+ params, using ThreadPoolExecutor

---

## Default Parameter Values Are Dangerous

> **Scope:** This rule applies to **function definitions** (`def foo(bar: bool = False)`), NOT to
> **function calls** where you pass an argument named `default` (e.g.,
> `click.confirm(default=True)`). Passing `default=True` to a function that accepts a `default`
> parameter is perfectly valid—you're not creating a default parameter value, you're explicitly
> providing a value.

**Avoid default parameter values unless absolutely necessary.** They are a significant source of
bugs.

**Why defaults are dangerous:**

1. **Silent incorrect behavior** - Callers forget to pass a parameter and get unexpected results
2. **Hidden coupling** - The default encodes an assumption that may not hold for all callers
3. **Audit difficulty** - Hard to verify all call sites are using the right value
4. **Refactoring hazard** - Adding a new parameter with a default doesn't trigger errors at existing
   call sites

```python
# DANGEROUS: Default that might be wrong for some callers
def process_file(path: Path, encoding: str = "utf-8") -> str:
    return path.read_text(encoding=encoding)

# Caller forgets encoding, silently gets wrong behavior for legacy file
content = process_file(legacy_latin1_file)  # Bug: should be encoding="latin-1"

# SAFER: Require explicit choice
def process_file(path: Path, encoding: str) -> str:
    return path.read_text(encoding=encoding)

# Caller must think about encoding
content = process_file(legacy_latin1_file, encoding="latin-1")
```

**When you discover a default is never overridden, eliminate it:**

```python
# If every call site uses the default...
activate_worktree(ctx, repo, path, script, "up", preserve_relative_path=True)  # Always True
activate_worktree(ctx, repo, path, script, "down", preserve_relative_path=True)  # Always True

# CORRECT: Remove the parameter entirely
def activate_worktree(ctx, repo, path, script, command_name) -> None:
    # Always preserve relative path - it's just the behavior
    ...
```

**Acceptable uses of defaults:**

1. **Truly optional behavior** - Where the default is correct for 95%+ of callers
2. **Backwards compatibility** - When adding a parameter to existing API (temporary)
3. **Test helper functions** - Functions in `tests/test_utils/` that exist to reduce test
   boilerplate are explicitly exempt. These helpers often wrap complex constructors (like
   `format_plan_header_body`) with sensible defaults, and having many default parameters is their
   intended purpose—not a code smell

**When reviewing code with defaults, ask:**

- Do all call sites actually want this default?
- Would a caller forgetting this parameter cause a bug?
- Is there a safer design that makes the choice explicit?

---

## Keyword-Only Arguments for Complex Functions

**Functions with 5 or more parameters MUST use keyword-only arguments.**

Use the `*` separator after the first positional parameter to enforce keyword-only at the language
level. This improves call-site readability by forcing explicit parameter names.

```python
# CORRECT: Keyword-only after first param
def fetch_data(
    url,
    *,
    timeout: float,
    retries: int,
    headers: dict[str, str],
    auth_token: str,
) -> Response:
    ...

# Call site is self-documenting
response = fetch_data(
    api_url,
    timeout=30.0,
    retries=3,
    headers={"Accept": "application/json"},
    auth_token=token,
)

# WRONG: All positional parameters
def fetch_data(
    url,
    timeout: float,
    retries: int,
    headers: dict[str, str],
    auth_token: str,
) -> Response:
    ...

# Call site is unreadable - what do these values mean?
response = fetch_data(api_url, 30.0, 3, {"Accept": "application/json"}, token)
```

**Exceptions:**

1. **`self`** - Always positional (Python requirement)
2. **`ctx` / context objects** - Can remain positional as the first parameter (convention)
3. **ABC/Protocol methods** - Exempt to avoid forcing all implementations to change signatures
4. **Click callbacks** - Click injects parameters; follow Click conventions

```python
# CORRECT: ctx stays positional, rest are keyword-only
def create_worktree(
    ctx: ErkContext,
    *,
    branch_name: str,
    base_branch: str,
    path: Path,
    checkout: bool,
) -> WorktreeInfo:
    ...
```

---

## ThreadPoolExecutor.submit() Pattern

`ThreadPoolExecutor.submit()` passes arguments positionally to the callable. For functions with
keyword-only parameters, wrap the call in a lambda:

```python
# WRONG: submit() passes args positionally - fails with keyword-only functions
future = executor.submit(fetch_data, url, timeout, retries, headers, token)

# CORRECT: Lambda enables keyword arguments
future = executor.submit(
    lambda: fetch_data(
        url,
        timeout=timeout,
        retries=retries,
        headers=headers,
        auth_token=token,
    )
)
```

---

## Speculative Test Infrastructure

**Don't add parameters to fakes "just in case" they might be useful for testing.**

Fakes should mirror production interfaces. Adding test-only configuration knobs that never get used
creates dead code and false complexity.

```python
# WRONG: Test-only parameter that's never used in production
class FakeGitHub:
    def __init__(
        self,
        prs: dict[str, PullRequestInfo] | None = None,
        rate_limited: bool = False,  # "Might test this later"
    ) -> None:
        self._rate_limited = rate_limited  # Never set to True anywhere

# CORRECT: Only add infrastructure when you need it
class FakeGitHub:
    def __init__(
        self,
        prs: dict[str, PullRequestInfo] | None = None,
    ) -> None:
        ...
```

**The test for this:** If grep shows a parameter is only ever passed in test files, and those tests
are testing hypothetical scenarios rather than actual production behavior, delete both the parameter
and the tests.

---

## Speculative Tests

```python
# FORBIDDEN: Tests for future features
# def test_feature_we_might_add():
#     pass

# CORRECT: TDD for current implementation
def test_feature_being_built_now():
    result = new_feature()
    assert result == expected
```

---

## Decision Checklist

Before adding a default parameter value:

- [ ] Do 95%+ of callers actually want this default?
- [ ] Would forgetting to pass this parameter cause a subtle bug?
- [ ] Is there a safer design that makes the choice explicit?
- [ ] If the default is never overridden anywhere, should this parameter exist at all?

**Default: Require explicit values; eliminate unused defaults**

Before adding a function with 5+ parameters:

- [ ] Have I added `*` after the first (or ctx) parameter?
- [ ] Is only `self`/`ctx` positional?
- [ ] Is this an ABC/Protocol method? (exempt from rule)
- [ ] If using ThreadPoolExecutor.submit(), am I using a lambda wrapper?

**Default: All parameters after the first should be keyword-only**

```

### references/checklists.md

```markdown
---
description: All decision checklists consolidated for final review before committing Python changes.
---

# Decision Checklists Reference

**Read when**: Final review before committing Python code, need quick lookup of requirements

---

## Before Writing `try/except`

- [ ] Is this at an error boundary? (CLI/API level)
- [ ] Can I check the condition proactively? (LBYL)
- [ ] Am I adding meaningful context, or just hiding?
- [ ] Is third-party API forcing me to use exceptions? (No LBYL check exists—not even format
      validation)
- [ ] Have I encapsulated the violation?
- [ ] Am I catching specific exceptions, not broad?
- [ ] If catching at error boundary, am I logging/warning? (Never silently swallow)

**Default: Let exceptions bubble up**

---

## Before Path Operations

- [ ] Did I check `.exists()` before `.resolve()`?
- [ ] Did I check `.exists()` before `.is_relative_to()`?
- [ ] Am I using `pathlib.Path`, not `os.path`?
- [ ] Did I specify `encoding="utf-8"`?

---

## Before Using `typing.cast()`

- [ ] Have I added a runtime assertion to verify the cast?
- [ ] Is the assertion cost trivial (O(1))? If yes, always add it.
- [ ] If skipping, is it because I just performed an isinstance check (redundant)?
- [ ] If skipping for performance, have I documented the measured overhead?

**Default: Always add runtime assertion before cast when cost is trivial**

---

## Before Defining an Interface (ABC or Protocol)

- [ ] Do I own all implementations? -> Prefer ABC
- [ ] Am I wrapping a third-party library? -> Prefer Protocol
- [ ] Do I need runtime isinstance() validation? -> Use ABC
- [ ] Is this a minimal interface (1-2 methods)? -> Protocol may be simpler
- [ ] Do I need shared method implementations? -> Use ABC

**Default for erk internal code: ABC. Default for external library facades: Protocol.**

---

## Before Preserving Backwards Compatibility

- [ ] Did the user explicitly request it?
- [ ] Is this a public API with external consumers?
- [ ] Have I documented why it's needed?
- [ ] Is migration cost prohibitively high?

**Default: Break the API and migrate callsites immediately**

---

## Before Inline Imports

- [ ] Is this to break a circular dependency?
- [ ] Is this for TYPE_CHECKING?
- [ ] Is this for conditional features?
- [ ] If for startup time: Have I MEASURED the import cost?
- [ ] If for startup time: Is the cost significant (>100ms)?
- [ ] If for startup time: Have I documented the measured cost in a comment?
- [ ] Have I documented why the inline import is needed?

**Default: Module-level imports**

---

## Before Importing/Re-Exporting Symbols

- [ ] Is there already a canonical location for this symbol?
- [ ] Am I creating a second import path for the same symbol?
- [ ] If this is a shim module, am I importing only what's needed for this module's purpose?
- [ ] Have I avoided `__all__` exports?

**Default: Import from canonical location, never re-export**

---

## Before Declaring a Local Variable

- [ ] Is this variable used more than once?
- [ ] Is this variable used close to where it's declared?
- [ ] Would inlining the computation hurt readability?
- [ ] Am I extracting object fields into locals that are only used once?

**Default: Inline single-use computations at the call site; access object attributes directly**

---

## Before Adding a Default Parameter Value

- [ ] Do 95%+ of callers actually want this default?
- [ ] Would forgetting to pass this parameter cause a subtle bug?
- [ ] Is there a safer design that makes the choice explicit?
- [ ] If the default is never overridden anywhere, should this parameter exist at all?

**Default: Require explicit values; eliminate unused defaults**

---

## Before Adding a Function with 5+ Parameters

- [ ] Have I added `*` after the first (or ctx) parameter?
- [ ] Is only `self`/`ctx` positional?
- [ ] Is this an ABC/Protocol method? (exempt from rule)
- [ ] If using ThreadPoolExecutor.submit(), am I using a lambda wrapper?

**Default: All parameters after the first should be keyword-only**

---

## Before Writing Module-Level Code

- [ ] Does this involve any computation (even `Path()` construction)?
- [ ] Does this involve I/O (file, network, environment)?
- [ ] Could this fail or raise exceptions?
- [ ] Would tests need to mock this value?

If any answer is "yes", wrap in a `@cache`-decorated function instead.

```

dignified-python | SkillHub