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>
3.0 KiB
3.0 KiB
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:
- Creating the exception object (~40 ns)
- Capturing the traceback (~70 ns)
- Stack unwinding and handler lookup (~30 ns)
EAFP vs LBYL: When to Use Which
EAFP (Easier to Ask Forgiveness than Permission)
try:
value = data[key]
except KeyError:
value = default
Use when: Exceptions are rare (<5% of calls)
LBYL (Look Before You Leap)
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
# Best: Use .get() - 26.3 ns, no exception possible
config = settings.get('database', {})
Practical Rules for Coding Agents
- try/except blocks are free - don't avoid them for performance
- Raising exceptions costs 6.5x - only raise for truly exceptional cases
- Use .get() for dicts - beats both EAFP and LBYL
- Return Optional for expected missing - not exceptions
- EAFP for file ops - TOCTOU protection matters more than perf
- LBYL when failures are common (>15% of calls)
- 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