Python105 min total · 18 parts
Python Fundamentals for Interviews: Data Structures, Comprehensions, and Gotchas
Part 12 of 18 · ~3 min
Exception Handling
try:
entry = Report.parse_line(raw_line)
except ValueError as e:
print(f"Bad status or duration: {e}")
except IndexError as e:
print(f"Line is missing fields: {e}")
else:
entries.append(entry) # only runs if parsing succeeded
finally:
lines_processed += 1 # always runs — bookkeeping goes here
Python walks except clauses in the order they're written and stops at the first one whose type matches, ignoring every clause below it entirely — it never keeps scanning for a "better" match further down. Since ValueError and IndexError are both subclasses of Exception, putting except Exception above either of them would catch everything on its own and leave the two specific clauses dead code, silently never reached, no matter how correctly they're written. The narrower exception types have to sit above the broader one for either to ever fire.
Bare except: is almost always wrong
The watcher eventually grows a "watch mode" that tails a live log forever, reading new lines as they arrive:
while True:
try:
process_next_line()
except: # catches EVERYTHING — including your own Ctrl+C
pass
Writing except: with nothing after it is shorthand for except BaseException:, and BaseException sits above everything in Python's exception hierarchy — not just the errors your code produces, but the two signals the interpreter uses specifically to shut things down on purpose: KeyboardInterrupt, raised when someone hits Ctrl+C, and SystemExit. Catch those alongside your genuine bugs and pressing Ctrl+C on a runaway watcher does nothing at all — the except block eats the interrupt and the loop starts its next iteration like nothing happened. Exception sits one level below BaseException in that hierarchy specifically so it can exclude those two; writing except Exception: gets you the "catch nearly anything" behavior people actually want from a bare except, without also disabling the ability to stop the program.
Custom exceptions and re-raising
class MalformedLogEntry(Exception):
def __init__(self, raw_line, reason):
super().__init__(f"Cannot parse {raw_line!r}: {reason}")
self.raw_line = raw_line
self.reason = reason
def parse_line(line):
parts = line.split()
if len(parts) < 6:
raise MalformedLogEntry(line, "expected 6 fields")
# ...
try:
entry = parse_line(raw)
except MalformedLogEntry as e:
log.error("dropping unparseable line: %s", e.reason)
raise # re-raises the SAME exception, original traceback intact
This bare raise is doing something genuinely different from the one that broke the retry decorator, and the difference is entirely about where it sits. Inside an active except block, Python knows exactly which exception is currently being handled, and a plain raise with nothing after it sends that same exception back out, traceback and all, as though this except had never caught it. Swap it for raise e and the exception object is identical, but its traceback now gets a new entry pointing at this raise line — which reads, to whoever debugs it later, as if the failure started here rather than three calls deep in parse_line. And because MalformedLogEntry carries raw_line and reason as real attributes rather than folding everything into one string, whatever catches it further up can act on why it failed without parsing English out of an error message.
There's a third option between "re-raise the same thing" and "raise something new that hides where it came from": chain them explicitly with raise ... from.
def parse_status(field):
try:
return int(field)
except ValueError as e:
raise MalformedLogEntry(field, "status must be numeric") from e
MalformedLogEntry is genuinely a different exception carrying different information than the ValueError that caused it, so replacing it outright is correct. from e keeps the original attached as __cause__ instead of throwing it away, and Python's default traceback prints both, chained, with "The above exception was the direct cause of the following exception" between them — which turns "why did parsing fail" from a debugging session into a single glance at the log.