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. ---
Scanned 6/1/2026
Install via CLI
openskills install tools-only/X-Skills---
---
# 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`.
---
## 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`
No comments yet. Be the first to comment!