Exception Mechanics: EAFP vs LBYL
Error as Values vs Exceptions:
Section titled “Error as Values vs Exceptions:”In Go, errors are just values. You treat them like integers or strings. They are explicit in the function signature: func foo() (int, error). You cannot ignore them easily; you have to assign them to _ or handle them. In Python, exceptions are events. They are implicit. A function signature def foo(): gives you zero warning that it might explode.
The “Hidden” Nature of Exceptions:
Section titled “The “Hidden” Nature of Exceptions:”You cannot know if a function will raise an exception just by looking at its signature.. If you call read_text(), it might work, or it might raise FileNotFoundError, PermissionError, MemoryError, or KeyboardInterrupt.
How do you survive this?
Section titled “How do you survive this?”Documentation:
Section titled “Documentation:”Good libraries (like pathlib) document exactly what exceptions they raise.
EAFP Principle:
Section titled “EAFP Principle:”- Python follows the philosophy: “It’s Easier to Ask Forgiveness than Permission.”
- Go/C style (LBYL - Look Before You Leap): Check if file exists -> Check permissions -> Open file.
- Python style (EAFP): Just try to open it. If it fails, catch the specific error.
How Python Handles Exceptions (The Internals):
Section titled “How Python Handles Exceptions (The Internals):”Since Python is interpreted, it handles exceptions using a mechanism called Stack Unwinding. When you run a Python script, the interpreter creates a “Frame Stack” (a pile of active function calls). The Process:
- The Trigger: A line of code causes an error (e.g.,
open('missing.txt')). - The Object: Python pauses execution and instantiates an Exception Object (e.g.,
FileNotFoundError()). - The Search (Bubbling):
- The interpreter checks the current function (stack frame): “Is this line inside a
try/exceptblock?” - No? It destroys the current stack frame (pops it off) and moves down to the caller function.
- It checks the caller: “Did you wrap this call in a
try/except?” - No? Pop that frame too. Go to the caller’s caller.
- The interpreter checks the current function (stack frame): “Is this line inside a
The Catch (or Crash):
Section titled “The Catch (or Crash):”- If found: Execution jumps immediately to the
exceptblock. The stack frames above it are gone forever. - If not found (Root): If it reaches the main module and nobody handled it, Python prints the Traceback (the history of the stack unwinding) to stderr and terminates the process.
Visualizing the Difference
Section titled “Visualizing the Difference”Go (Linear Flow): The error is handed back up one step at a time. Every function explicitly decides to pass it up or handle it.
result, err := Step1() if err != nil { return err } // Explicit hand-offPython (Teleportation): The exception blasts through the stack until it hits a “safety net” (except).
try: step1() # If this fails deep inside, it jumps straight to 'except' except Error: handle_it()Exception Hierarchy:
Section titled “Exception Hierarchy:”In Python, Exceptions are Classes. They inherit from each other. This is powerful because you can choose how “wide” your safety net is.
BaseException(The root)Exception(Standard errors)OSError(System errors)FileNotFoundErrorPermissionError
ValueErrorTypeError
Why this matters:
Section titled “Why this matters:”- If you catch
OSError, you catch both missing files and permission errors. - If you catch
FileNotFoundError, you let permission errors crash the program (which might be what you want!).
How does one make the this stack trace more readable, especially because the “unwinding” will keep happening until the main is reached which means if any library code is being called in the middle then those function calls also get unwinded which may not be very helpful during debugging unless one is writing those libs.
Exception Chaining:
Section titled “Exception Chaining:”(raise ... from ...). This allows you to catch a low-level “library error” and wrap it in a clean “application error” before passing it up. Here is how you make stack traces readable:
# Define a custom error for YOUR applicationclass MyConfigFileError(Exception): pass
def read_config(): try: # 1. This might fail deep inside Python's library code return Path("config.ini").read_text() except FileNotFoundError as e: # 2. We catch the low-level noise. # 3. We create a high-level, readable error. # 4. The 'from e' keeps the original trace hidden but linked for debugging. raise MyConfigFileError("Could not load the system configuration.") from e
# Main programtry: read_config()except MyConfigFileError as e: # The user just sees this clean message print(f"Critical Error: {e}")What happens here?
- The User sees:
Critical Error: Could not load the system configuration. - The Developer (You) sees: If you remove the
try/exceptin main, Python prints the new error, but also says “The above exception was the direct cause of the following exception,” showing you the original crash so you can still debug it.
The Fear of EAFP (“I can’t catch them all!”): Is it impossible to catch all exceptions? Technically, no. But we can catch them all using the root bucket:
try: path.read_text()except Exception: # Catches literally everything print("Something bad happened.")The Golden Rule: Only catch exceptions you can recover from. If your read_file function fails because the file is missing:
Can you recover? (e.g., create a default file? ask the user for a new name?)
- Yes: Catch
FileNotFoundErrorand do that logic. - No: Let it crash.
Why let it crash? If a file is locked due to a Permission Error and you didn’t write code to handle that specific case, your program is broken. It should exit with a stack trace. Hiding that error (swallowing exceptions) is worse because your program continues running in an undefined state.