Introduction
In many languages, exceptions are reserved for truly exceptional situations. In Python, they are a first-class control flow mechanism. This lesson explains the EAFP philosophy, how iterators use StopIteration, and how try/except/else/finally gives you fine-grained control over success and failure paths.
Key Concepts
- EAFP (Easier to Ask Forgiveness than Permission): Try an operation and handle failure, rather than checking preconditions.
- LBYL (Look Before You Leap): Check conditions before acting -- less Pythonic but sometimes appropriate.
StopIteration: The exception that signals the end of an iterator.try/except/else/finally: The full exception handling block with success and cleanup paths.
Real World Context
The EAFP pattern is everywhere in production Python: accessing dictionary keys, opening files, parsing user input, and connecting to databases. It is also more efficient in the common case because checking for permission (LBYL) can be slower or introduce race conditions -- for example, checking if a file exists and then opening it leaves a window where the file can be deleted between the check and the open.
Deep Dive
EAFP vs LBYL
Python favors EAFP (Easier to Ask Forgiveness than Permission) over LBYL (Look Before You Leap).
python# LBYL (non-Pythonic) if key in dictionary: value = dictionary[key] else: value = default # EAFP (Pythonic) try: value = dictionary[key] except KeyError: value = default # Even better value = dictionary.get(key, default)
StopIteration for Iterators
pythonit = iter([1, 2, 3]) while True: try: value = next(it) print(value) except StopIteration: break
Using else with try
pythontry: result = risky_operation() except SomeError: handle_error() else: # Only runs if no exception process(result) finally: # Always runs cleanup()
Raising Exceptions
pythondef divide(a, b): if b == 0: raise ValueError("Cannot divide by zero") return a / b # Re-raising try: something() except Exception: log_error() raise # Re-raises the caught exception # Exception chaining try: something() except OriginalError as e: raise NewError("Context") from e
Common Pitfalls
- Catching too broad an exception -- Using bare
except:orexcept Exception:swallows errors you did not anticipate, making debugging much harder. Catch the specific exception type you expect. - Putting too much code in the
tryblock -- Only wrap the line that might raise the exception. Extra code insidetrycan catch unrelated errors and mask bugs. - Forgetting to re-raise after logging -- If you catch an exception just to log it, always
raiseto propagate it. Silencing exceptions leads to silent failures in production.
Best Practices
- Use EAFP when the exceptional case is rare -- If the key is usually present,
try/except KeyErroris faster than checkingif key in dicton every call. - Put success-path code in the
elseblock -- This makes it clear what runs only when thetryblock succeeds and avoids accidentally catching new exceptions.
Summary
- Python's EAFP philosophy favors trying an operation and catching failures over checking preconditions.
StopIterationis used internally by iterators andforloops to signal completion.- The
elseclause ontryruns only when no exception was raised;finallyalways runs. - Catch specific exception types, not broad
Exception, to avoid masking bugs. - Re-raise exceptions after logging to avoid silent failures.
Code Examples
# Custom exceptions
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
# Using it
try:
if not email:
raise ValidationError("email", "Required field")
except ValidationError as e:
print(f"Validation failed: {e.field} - {e.message}")