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

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:

  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)

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

  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