Files
python-performance-adrs/papers/exception-flow.md
dafit 7efd1368d1 feat: Add 8 domain papers and RULEBOOK.md
Domain papers distilled from python-numbers-everyone-should-know:
- async-overhead: 1,400x sync vs async overhead
- collection-membership: 200x set vs list at 1000 items
- json-serialization: 8x orjson vs stdlib
- exception-flow: 6.5x exception overhead (try/except free)
- string-formatting: f-strings > % > .format()
- memory-slots: 69% memory reduction with __slots__
- import-optimization: 100ms+ for heavy packages
- database-patterns: 98% commit overhead in SQLite

RULEBOOK.md: ~200 token distillation for coding subagents

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-03 14:31:40 +01:00

112 lines
3.0 KiB
Markdown

# Exception Flow: Performance Patterns
**Domain:** Exception handling overhead
**Source:** python-numbers-everyone-should-know benchmarks (Python 3.14.2, Apple Silicon)
---
## TL;DR
- **try/except with no exception**: Nearly free (1.1 ns overhead)
- **Raising an exception**: 6.5x slower than the happy path (139 ns vs 21.5 ns)
- **EAFP is fine when exceptions are rare** (<5% of calls)
- **Use LBYL for expected failures** (dict key lookup, file existence)
- **Never use exceptions for normal control flow**
---
## The Numbers
### Happy Path (No Exception Raised)
| Operation | Time | Overhead vs Baseline |
|-----------|------|---------------------|
| Function call (no try/except) | 20.4 ns | baseline |
| try/except (no exception raised) | 21.5 ns | +1.1 ns (+5%) |
| try/except ValueError (specific) | 22.9 ns | +2.5 ns (+12%) |
| try/except/finally | 22.1 ns | +1.7 ns (+8%) |
**Key insight:** The try block itself is essentially free.
### Sad Path (Exception Raised)
| Operation | Time | Slowdown vs Happy Path |
|-----------|------|----------------------|
| raise + catch ValueError | 139 ns | **6.5x slower** |
| raise + catch (base Exception) | 140 ns | 6.5x slower |
| raise + catch custom exception | 146 ns | 6.8x slower |
| raise + catch with `as e` | 148 ns | 6.9x slower |
**Key insight:** The 6.5x overhead comes from:
1. Creating the exception object (~40 ns)
2. Capturing the traceback (~70 ns)
3. Stack unwinding and handler lookup (~30 ns)
---
## EAFP vs LBYL: When to Use Which
### EAFP (Easier to Ask Forgiveness than Permission)
```python
try:
value = data[key]
except KeyError:
value = default
```
**Use when:** Exceptions are rare (<5% of calls)
### LBYL (Look Before You Leap)
```python
if key in data:
value = data[key]
else:
value = default
```
**Use when:** The failure case is common (>15% of calls)
### Crossover Point
**Rule of thumb:** If exceptions occur more than 15% of the time, use LBYL.
### dict.get() Beats Both
```python
# Best: Use .get() - 26.3 ns, no exception possible
config = settings.get('database', {})
```
---
## Practical Rules for Coding Agents
1. **try/except blocks are free** - don't avoid them for performance
2. **Raising exceptions costs 6.5x** - only raise for truly exceptional cases
3. **Use .get() for dicts** - beats both EAFP and LBYL
4. **Return Optional for expected missing** - not exceptions
5. **EAFP for file ops** - TOCTOU protection matters more than perf
6. **LBYL when failures are common** (>15% of calls)
7. **Never use exceptions for control flow**
---
## Summary
| Scenario | Recommendation |
|----------|----------------|
| Exception rate <5% | EAFP (try/except) |
| Exception rate >15% | LBYL (check first) |
| Dict key lookup | Use `.get()` |
| Optional return value | Return `None`, not exception |
| File operations | EAFP (TOCTOU protection) |
| Control flow | Never use exceptions |
**The core insight:** try/except is free; raising is not. Design APIs to minimize raises, not to avoid try blocks.
---
*Benchmark source: python-numbers-everyone-should-know*