---
---

# Dignified Python - Core Standards

This document contains the core Python coding standards that apply to 80%+ of Python code. These principles are loaded with every skill invocation.

For conditional loading of specialized patterns:

- CLI development -> Load `cli-patterns.md`
- Subprocess operations -> Load `subprocess.md`

For detailed reference material, see the "When to Read Each Reference" section in SKILL.md.

---

## The Cornerstone: LBYL Over EAFP

**Look Before You Leap: Check conditions proactively, NEVER use exceptions for control flow.**

This is the single most important rule in dignified Python. Every pattern below flows from this principle.

```python
# CORRECT: Check first
if key in mapping:
    value = mapping[key]
    process(value)

# WRONG: Exception as control flow
try:
    value = mapping[key]
    process(value)
except KeyError:
    pass
```

---

## Exception Handling Basics

### Core Principle

**ALWAYS use LBYL, NEVER EAFP for control flow**

LBYL means checking conditions before acting. EAFP (Easier to Ask for Forgiveness than Permission) means trying operations and catching exceptions. In dignified Python, we strongly prefer LBYL.

### Dictionary Access Patterns

```python
# CORRECT: Membership testing
if key in mapping:
    value = mapping[key]
    process(value)
else:
    handle_missing()

# ALSO CORRECT: .get() with default
value = mapping.get(key, default_value)
process(value)

# CORRECT: Check before nested access
if "config" in data and "timeout" in data["config"]:
    timeout = data["config"]["timeout"]

# WRONG: KeyError as control flow
try:
    value = mapping[key]
except KeyError:
    handle_missing()
```

### 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**

**Default: Let exceptions bubble up**

For detailed exception handling patterns including B904 chaining, third-party API examples, and anti-patterns, see `references/exception-handling.md`.

---

## Path Operations

### The Golden Rule

**ALWAYS check `.exists()` BEFORE `.resolve()` or `.is_relative_to()`**

### Why This Matters

- `.resolve()` raises `OSError` for non-existent paths
- `.is_relative_to()` raises `ValueError` for invalid comparisons
- Checking `.exists()` first avoids exceptions entirely (LBYL!)

### Correct Patterns

```python
from pathlib import Path

# CORRECT: Check exists first
for wt_path in worktree_paths:
    if wt_path.exists():
        wt_path_resolved = wt_path.resolve()
        if current_dir.is_relative_to(wt_path_resolved):
            current_worktree = wt_path_resolved
            break

# WRONG: Using exceptions for path validation
try:
    wt_path_resolved = wt_path.resolve()
    if current_dir.is_relative_to(wt_path_resolved):
        current_worktree = wt_path_resolved
except (OSError, ValueError):
    continue
```

### Pathlib Best Practices

**Always Use Pathlib (Never os.path)**

```python
# CORRECT: Use pathlib.Path
from pathlib import Path

config_file = Path.home() / ".config" / "app.yml"
if config_file.exists():
    content = config_file.read_text(encoding="utf-8")

# WRONG: Use os.path
import os.path
config_file = os.path.join(os.path.expanduser("~"), ".config", "app.yml")
```

**Always Specify Encoding**

```python
# CORRECT: Always specify encoding
content = path.read_text(encoding="utf-8")
path.write_text(data, encoding="utf-8")

# WRONG: Default encoding
content = path.read_text()  # Platform-dependent!
```

---

## Import Organization

### 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** (circular deps, TYPE_CHECKING, conditional features)

```python
# CORRECT: Module-level imports
import json
import click
from pathlib import Path
from erk.config import load_config

def my_function() -> None:
    data = json.loads(content)

# CORRECT: Absolute import
from erk.config import load_config

# WRONG: Relative import
from .config import load_config

# WRONG: Inline imports without justification
def my_function() -> None:
    import json  # NEVER do this
```

For detailed inline import patterns and when they're legitimate, see `references/module-design.md`.

### No Import Aliases

**Rule:** Imports must use canonical names. The `as` keyword for aliasing is prohibited by default.

**Rationale:** Import aliases obscure the canonical name, make grep-based discovery harder, and invite cargo-culting. When reading code, aliases force you to scroll up to find the real import, breaking flow. When searching for usage of a symbol, aliases fragment results across different names.

**The only acceptable exception:** Resolving genuine name collisions between two different modules that export identically-named symbols.

```python
# WRONG: Gratuitous aliasing
from erk.helpers import require_issues as require_github_issues

# CORRECT: Use canonical name
from erk.helpers import require_issues

# CORRECT (rare exception): Resolving genuine collision
from datetime import datetime as dt
from pandas import datetime as pd_datetime
```

**Note:** This rule is complementary to the "No Re-Exports" rule. Re-exports use `import X as X` for explicit re-export syntax at package boundaries. This rule targets gratuitous renaming aliases.

---

## Performance Guidelines

### Properties Must Be O(1)

```python
# WRONG: Property doing I/O
@property
def size(self) -> int:
    return self._fetch_from_db()

# CORRECT: Explicit method name
def fetch_size_from_db(self) -> int:
    return self._fetch_from_db()

# CORRECT: O(1) property
@property
def size(self) -> int:
    return self._cached_size
```

### Magic Methods Must Be O(1)

```python
# WRONG: __len__ doing iteration
def __len__(self) -> int:
    return sum(1 for _ in self._items)

# CORRECT: O(1) __len__
def __len__(self) -> int:
    return self._count
```

---

## Anti-Patterns

### No Backwards Compatibility Preservation (Default)

```python
# WRONG: Keeping old API unnecessarily
def process_data(data: dict, legacy_format: bool = False) -> Result:
    if legacy_format:
        return legacy_process(data)
    return new_process(data)

# CORRECT: Break and migrate immediately
def process_data(data: dict) -> Result:
    return new_process(data)
```

### No Re-Exports: One Canonical Import Path

**Core Principle:** Every symbol has exactly one import path. Never re-export.

```python
# WRONG: __all__ exports create duplicate import paths
# myapp/__init__.py
from myapp.core import Process
__all__ = ["Process"]

# CORRECT: Empty __init__.py, import from canonical location
# from myapp.core import Process
```

**When re-exports ARE required** (plugin entry points): Use explicit `import X as X` syntax:

```python
# CORRECT: Explicit re-export syntax for required entry points
from myapp.core.feature import my_function as my_function
```

### Declare Variables Close to Use

```python
# WRONG: Variable declared far from use
def process_data(ctx, items):
    result_path = compute_result_path(ctx)  # Declared here...
    # 20+ lines of other logic...
    save_to_path(transformed, result_path)  # ...used here

# CORRECT: Inline at use site
def process_data(ctx, items):
    validate_items(items)
    transformed = transform_items(items)
    save_to_path(transformed, compute_result_path(ctx))
```

### Don't Destructure Objects Into Single-Use Locals

```python
# WRONG: Unnecessary field extraction
result = fetch_user(user_id)
name = result.name      # only used once below
email = result.email    # only used once below
send_notification(name, email, role)

# CORRECT: Access fields directly
user = fetch_user(user_id)
send_notification(user.name, user.email, user.role)
```

### Indentation Depth Limit

**Maximum indentation: 4 levels**

```python
# WRONG: Too deeply nested (5 levels)
def process_items(items):
    for item in items:
        if item.valid:
            for child in item.children:
                if child.enabled:
                    for grandchild in child.descendants:
                        pass  # 5 levels deep!

# CORRECT: Extract helper functions
def process_items(items):
    for item in items:
        if item.valid:
            process_children(item.children)

def process_children(children):
    for child in children:
        if child.enabled:
            process_descendants(child.descendants)
```

---

## Lightweight `__init__` Pattern

Class `__init__` methods should be lightweight and cheap - just data assignment. Heavy I/O operations belong in static factory methods.

### Why

- **Testability**: Tests can construct objects directly without I/O setup
- **Predictability**: Instantiation has no side effects
- **Explicit dependencies**: Heavy operations are visible at call sites
- **Flexibility**: Multiple construction paths (from file, from dict, for testing)

### Correct Pattern

```python
class Registry:
    def __init__(self, items: list[Item], default: str | None) -> None:
        """Lightweight - just assigns data."""
        self._items = {item.name: item for item in items}
        self._default = default

    @classmethod
    def from_config_path(cls, path: Path) -> "Registry":
        """Heavy I/O happens here, not in __init__."""
        data = tomllib.loads(path.read_text())
        items = [Item.from_dict(d) for d in data.get("items", [])]
        return cls(items=items, default=data.get("default"))
```

### Anti-pattern

```python
# WRONG: __init__ does heavy I/O
class Registry:
    def __init__(self, config_path: Path) -> None:
        data = tomllib.loads(config_path.read_text())  # Heavy I/O
        self._items = ...
```

### When This Applies

- Any class backed by file storage (TOML, JSON, etc.)
- Any class that calls subprocess on construction
- Any class that makes network requests on construction
- Any class where tests need to verify behavior without I/O

---

## Backwards Compatibility Philosophy

**Default stance: NO backwards compatibility preservation**

Only preserve backwards compatibility when:

- Code is clearly part of public API
- User explicitly requests it
- Migration cost is prohibitively high (rare)

Benefits:

- Cleaner, maintainable codebase
- Faster iteration
- No legacy code accumulation
- Simpler mental models

---

## See Also

For detailed guidance on specialized topics:

- **Exception chaining (B904)**: `references/exception-handling.md`
- **ABC vs Protocol**: `references/interfaces.md`
- **typing.cast() assertions**: `references/typing-advanced.md`
- **Import-time side effects, @cache**: `references/module-design.md`
- **Default parameters, keyword-only args**: `references/api-design.md`
- **All decision checklists**: `references/checklists.md`
