# The Cornerstone: LBYL Over EAFP

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

- Skill: `tools-only/the-cornerstone-lbyl-over-eafp` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add tools-only/the-cornerstone-lbyl-over-eafp`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tools-only/the-cornerstone-lbyl-over-eafp/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: tools-only (https://skillmd.com/u/tools-only)
- Updated: 2026-09-29
- Page: https://skillmd.com/skills/tools-only/the-cornerstone-lbyl-over-eafp

---


## 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`.

---

## 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)
```

---

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

