beps/docs/proposals/BEP-001-exceptions/legacy-ignore/context/python.md
"It's Easier to Ask for Forgiveness than Permission" (EAFP). In Python, exceptions are not just for errors; they are for control flow. It is idiomatic to try an operation and catch the failure rather than checking preconditions.
LBYL (Look Before You Leap):
if os.path.exists(file):
open(file)
EAFP (Pythonic):
try:
open(file)
except FileNotFoundError:
pass
Why? LBYL has a race condition (file deleted between check and open). EAFP is atomic.
else BlockPython's try-except-else-finally is unique. else runs only if no exception was raised.
try:
data = read_file()
except OSError:
handle_error()
else:
# Only runs if read_file succeeded.
# Good because exceptions here are NOT caught by the except block above.
process(data)
Python 3 automatically chains exceptions to preserve context.
try:
...
except ValueError as e:
raise RuntimeError("Processing failed") from e
This results in a traceback saying "The above exception was the direct cause of the following exception".
with)The with statement abstracts try-finally logic.
with open("file.txt") as f:
data = f.read()
# f is closed automatically, even if read() fails
Tradeoff: Entering a try block is very cheap (almost zero cost in modern Python).
Tradeoff: Catching by type.
except LookupError catches both IndexError (lists) and KeyError (dicts).except Exception might catch KeyboardInterrupt (if not careful, though BaseException separates system exits).Tradeoff: No declared throws.
Python optimizes for developer speed and readability. It accepts runtime risk and performance overhead (on error) to keep code clean and logic straightforward.