Python Exception Hierarchy Practice Problems & Exercises
Practice: Exception Hierarchy
← Back to lessonEasy
Predict the output. An except OSError clause encounters a FileNotFoundError. Determine what gets caught and what isinstance reports.
try:
open("/nonexistent/path/config.json")
except OSError as e:
print("Caught:", type(e).__name__)
print(isinstance(e, FileNotFoundError))
print(isinstance(e, OSError))Solution
try:
open("/nonexistent/path/config.json")
except OSError as e:
print("Caught:", type(e).__name__)
print(isinstance(e, FileNotFoundError))
print(isinstance(e, OSError))
Output:
Caught: FileNotFoundError
True
True
How it works: Opening a nonexistent file raises FileNotFoundError. Since FileNotFoundError is a subclass of OSError, the except OSError clause catches it. The variable e holds the actual FileNotFoundError instance — type(e).__name__ reveals the real type. isinstance returns True for both the actual class and any ancestor class in the hierarchy.
Key insight: When you catch a parent exception, you catch every subclass in its subtree. The actual exception object retains its real type — catching via a parent does not change what the object is, only which handler intercepts it.
Expected Output
Caught: FileNotFoundError\nTrue\nTrueHints
Hint 1: FileNotFoundError is a subclass of OSError. Catching a parent catches all its children.
Hint 2: isinstance returns True for both the exact class and any parent class in the hierarchy.
Predict the output. Check where SystemExit and ValueError sit in the hierarchy using issubclass.
print("SystemExit is BaseException:", issubclass(SystemExit, BaseException))
print("SystemExit is Exception:", issubclass(SystemExit, Exception))
print("ValueError is BaseException:", issubclass(ValueError, BaseException))
print("ValueError is Exception:", issubclass(ValueError, Exception))Solution
print("SystemExit is BaseException:", issubclass(SystemExit, BaseException))
print("SystemExit is Exception:", issubclass(SystemExit, Exception))
print("ValueError is BaseException:", issubclass(ValueError, BaseException))
print("ValueError is Exception:", issubclass(ValueError, Exception))
Output:
SystemExit is BaseException: True
SystemExit is Exception: False
ValueError is BaseException: True
ValueError is Exception: True
How it works: SystemExit inherits directly from BaseException, bypassing Exception. This means except Exception will NOT catch SystemExit. Meanwhile, ValueError inherits from Exception, which inherits from BaseException, so it is a subclass of both.
Key insight: This is why except Exception is safe for general error handling — it catches all program errors but lets SystemExit, KeyboardInterrupt, and GeneratorExit propagate. Using except BaseException or a bare except: is dangerous because it swallows termination signals.
Expected Output
SystemExit is BaseException: True\nSystemExit is Exception: False\nValueError is BaseException: True\nValueError is Exception: TrueHints
Hint 1: SystemExit, KeyboardInterrupt, and GeneratorExit inherit from BaseException directly — not from Exception.
Hint 2: All exceptions ultimately inherit from BaseException, so issubclass always returns True for BaseException.
Predict the output. A single except LookupError clause handles errors from both a list and a dictionary.
def safe_lookup(container, key):
try:
return container[key]
except LookupError as e:
print(type(e).__name__, "caught via LookupError")
return None
safe_lookup([10, 20, 30], 99)
safe_lookup({"a": 1, "b": 2}, "z")Solution
def safe_lookup(container, key):
try:
return container[key]
except LookupError as e:
print(type(e).__name__, "caught via LookupError")
return None
safe_lookup([10, 20, 30], 99)
safe_lookup({"a": 1, "b": 2}, "z")
Output:
IndexError caught via LookupError
KeyError caught via LookupError
How it works: Accessing [10, 20, 30][99] raises IndexError. Accessing {"a": 1, "b": 2}["z"] raises KeyError. Both are subclasses of LookupError, so the single except LookupError clause catches both. The type(e).__name__ reveals the actual exception type even though we caught via the parent.
Key insight: LookupError is the common ancestor for "asked for something that does not exist by index or key." This makes it ideal for writing generic container access functions that work with lists, dicts, tuples, and any custom container that follows the same convention.
Expected Output
IndexError caught via LookupError\nKeyError caught via LookupErrorHints
Hint 1: Both IndexError and KeyError are subclasses of LookupError.
Hint 2: Catching LookupError is useful when you want to handle any failed lookup uniformly.
Predict the output. Two different math errors are both caught by except ArithmeticError.
import math
errors = []
try:
result = 10 / 0
except ArithmeticError as e:
errors.append(type(e).__name__)
try:
result = math.exp(1000)
except ArithmeticError as e:
errors.append(type(e).__name__)
print(errors[0])
print(errors[1])
print(issubclass(ZeroDivisionError, ArithmeticError))Solution
import math
errors = []
try:
result = 10 / 0
except ArithmeticError as e:
errors.append(type(e).__name__)
try:
result = math.exp(1000)
except ArithmeticError as e:
errors.append(type(e).__name__)
print(errors[0])
print(errors[1])
print(issubclass(ZeroDivisionError, ArithmeticError))
Output:
ZeroDivisionError
OverflowError
True
How it works: 10 / 0 raises ZeroDivisionError. math.exp(1000) computes e^1000, which exceeds the float range and raises OverflowError. Both are subclasses of ArithmeticError, so the parent handler catches both. Note that Python integers never overflow — 2 ** 10000 works fine. OverflowError only occurs with float operations and C-level math functions.
Key insight: The ArithmeticError branch groups all math-related exceptions. Catching ArithmeticError is useful when you want to handle any numeric computation failure uniformly, such as in a calculator or math evaluation engine.
Expected Output
ZeroDivisionError\nOverflowError\nTrueHints
Hint 1: ZeroDivisionError and OverflowError are both subclasses of ArithmeticError.
Hint 2: Python ints never overflow — OverflowError only affects floats.
Medium
Predict the output. Three different handler orderings produce different results. Determine which handler catches each exception.
# Case 1: Child before parent (correct order)
try:
open("/no/such/file.txt")
except FileNotFoundError:
print("FileNotFoundError handler")
except OSError:
print("OSError handler")
# Case 2: Parent before child (child is unreachable)
try:
open("/no/such/file.txt")
except OSError:
print("OSError handler")
except FileNotFoundError:
print("FileNotFoundError handler")
# Case 3: Only the child handler
try:
open("/no/such/file.txt")
except FileNotFoundError:
print("FileNotFoundError handler")Solution
# Case 1: Child before parent (correct order)
try:
open("/no/such/file.txt")
except FileNotFoundError:
print("FileNotFoundError handler")
except OSError:
print("OSError handler")
# Case 2: Parent before child (child is unreachable)
try:
open("/no/such/file.txt")
except OSError:
print("OSError handler")
except FileNotFoundError:
print("FileNotFoundError handler")
# Case 3: Only the child handler
try:
open("/no/such/file.txt")
except FileNotFoundError:
print("FileNotFoundError handler")
Output:
FileNotFoundError handler
OSError handler
FileNotFoundError handler
How it works:
-
Case 1: Python checks
except FileNotFoundErrorfirst. The raisedFileNotFoundErrormatches, so this handler runs. Theexcept OSErrorclause is never reached. -
Case 2: Python checks
except OSErrorfirst. SinceFileNotFoundErroris a subclass ofOSError, it matches. Theexcept FileNotFoundErrorclause below is unreachable — it can never execute for anyOSErrorsubclass. -
Case 3: Only
FileNotFoundErroris caught. If aPermissionErrorwere raised instead, it would propagate uncaught.
Key insight: Always order except clauses from most specific (child) to most general (parent). Python evaluates them top-to-bottom and uses the first match. Placing a parent before its child makes the child handler dead code. Some linters warn about this, but Python itself does not raise an error.
Expected Output
FileNotFoundError handler\nOSError handler\nFileNotFoundError handlerHints
Hint 1: Python checks except clauses top-to-bottom and uses the first one that matches.
Hint 2: If you put the parent class first, the child handler below it will never execute.
Predict the output. Check how isinstance traverses the full inheritance chain for a ConnectionRefusedError.
e = ConnectionRefusedError("Connection refused")
print(isinstance(e, ConnectionRefusedError))
print(isinstance(e, ConnectionError))
print(isinstance(e, OSError))
print(isinstance(e, Exception))
print(isinstance(e, FileNotFoundError))
print(isinstance(e, LookupError))Solution
e = ConnectionRefusedError("Connection refused")
print(isinstance(e, ConnectionRefusedError))
print(isinstance(e, ConnectionError))
print(isinstance(e, OSError))
print(isinstance(e, Exception))
print(isinstance(e, FileNotFoundError))
print(isinstance(e, LookupError))
Output:
True
True
True
True
False
False
How it works: ConnectionRefusedError sits in this inheritance chain: ConnectionRefusedError -> ConnectionError -> OSError -> Exception -> BaseException. isinstance returns True for every class in this chain. It returns False for FileNotFoundError (a sibling under OSError, not an ancestor) and False for LookupError (a completely different branch under Exception).
Key insight: isinstance walks the full MRO (Method Resolution Order) upward. An exception is an instance of its own class AND every ancestor class. But it is NOT an instance of sibling classes or classes in other branches. This is why catching OSError catches ConnectionRefusedError but catching LookupError does not.
Expected Output
True\nTrue\nTrue\nTrue\nFalse\nFalseHints
Hint 1: isinstance checks the entire inheritance chain, not just the immediate parent.
Hint 2: ConnectionRefusedError inherits from ConnectionError, which inherits from OSError, which inherits from Exception, which inherits from BaseException.
Predict the output. Verify that the pre-3.3 exception names are now aliases for OSError.
# Are the old names the same class?
print(IOError is OSError)
print(EnvironmentError is OSError)
# Does catching IOError catch FileNotFoundError?
try:
open("/no/such/file.txt")
except IOError:
print(True)
# Is a FileNotFoundError also an IOError?
e = FileNotFoundError("gone")
print(isinstance(e, IOError))
# Is IOError a subclass of Exception?
print(issubclass(IOError, Exception))Solution
# Are the old names the same class?
print(IOError is OSError)
print(EnvironmentError is OSError)
# Does catching IOError catch FileNotFoundError?
try:
open("/no/such/file.txt")
except IOError:
print(True)
# Is a FileNotFoundError also an IOError?
e = FileNotFoundError("gone")
print(isinstance(e, IOError))
# Is IOError a subclass of Exception?
print(issubclass(IOError, Exception))
Output:
True
True
True
True
True
How it works: In Python 3.3, IOError and EnvironmentError were made aliases for OSError. The is check confirms they are literally the same object in memory — not just equivalent, but identical. Since IOError IS OSError, catching IOError catches all OSError subclasses including FileNotFoundError. Similarly, isinstance(e, IOError) is identical to isinstance(e, OSError).
Key insight: When reading legacy code, treat IOError and EnvironmentError as OSError. They are the same class. Modern code should use OSError (or its specific subclasses) for clarity. The old names exist solely for backward compatibility.
Expected Output
True\nTrue\nTrue\nTrue\nTrueHints
Hint 1: Since Python 3.3, IOError and EnvironmentError are aliases for OSError — they are the exact same class.
Hint 2: Old code that catches IOError works identically to catching OSError.
Predict the output. Compare what except Exception and except BaseException catch when SystemExit is raised.
import sys
# Case 1: except Exception catches ValueError
try:
raise ValueError("bad value")
except Exception:
print("Caught ValueError")
# Case 2: except Exception does NOT catch SystemExit
try:
try:
sys.exit(1)
except Exception:
print("Caught SystemExit with Exception")
except SystemExit:
print("SystemExit escaped")
# Case 3: except BaseException catches SystemExit
try:
sys.exit(1)
except BaseException:
print("Caught SystemExit with BaseException")Solution
import sys
# Case 1: except Exception catches ValueError
try:
raise ValueError("bad value")
except Exception:
print("Caught ValueError")
# Case 2: except Exception does NOT catch SystemExit
try:
try:
sys.exit(1)
except Exception:
print("Caught SystemExit with Exception")
except SystemExit:
print("SystemExit escaped")
# Case 3: except BaseException catches SystemExit
try:
sys.exit(1)
except BaseException:
print("Caught SystemExit with BaseException")
Output:
Caught ValueError
SystemExit escaped
Caught SystemExit with BaseException
How it works:
-
ValueErroris a subclass ofException, soexcept Exceptioncatches it. -
sys.exit(1)raisesSystemExit, which inherits fromBaseExceptiondirectly — NOT fromException. The innerexcept Exceptiondoes not match, soSystemExitpropagates to the outerexcept SystemExit. -
except BaseExceptioncatches everything, includingSystemExit. This is whyexcept BaseException(and bareexcept:) are dangerous — they prevent program termination.
Key insight: This is the fundamental design of Python's exception hierarchy. The BaseException / Exception split exists specifically so that except Exception is a safe catch-all for program errors without accidentally swallowing termination signals. Always use except Exception instead of bare except: or except BaseException.
Expected Output
Caught ValueError\nSystemExit escaped\nCaught SystemExit with BaseExceptionHints
Hint 1: except Exception catches all program errors but NOT SystemExit, KeyboardInterrupt, or GeneratorExit.
Hint 2: except BaseException catches everything, including termination signals.
Hard
Predict the output. A custom exception inherits from both ConnectionError and TimeoutError. Determine how isinstance and handler matching work.
class NetworkTimeoutError(ConnectionError, TimeoutError):
pass
e = NetworkTimeoutError("connection timed out")
print(type(e).__name__)
print(isinstance(e, ConnectionError))
print(isinstance(e, TimeoutError))
print(isinstance(e, OSError))
print(isinstance(e, Exception))
# Which handler catches it?
try:
raise NetworkTimeoutError("timed out")
except ConnectionError:
print("ConnectionError handler")
except TimeoutError:
print("TimeoutError handler")
except OSError:
print("OSError handler")Solution
class NetworkTimeoutError(ConnectionError, TimeoutError):
pass
e = NetworkTimeoutError("connection timed out")
print(type(e).__name__)
print(isinstance(e, ConnectionError))
print(isinstance(e, TimeoutError))
print(isinstance(e, OSError))
print(isinstance(e, Exception))
# Which handler catches it?
try:
raise NetworkTimeoutError("timed out")
except ConnectionError:
print("ConnectionError handler")
except TimeoutError:
print("TimeoutError handler")
except OSError:
print("OSError handler")
Output:
NetworkTimeoutError
True
True
True
True
ConnectionError handler
How it works: NetworkTimeoutError inherits from both ConnectionError and TimeoutError. Both of these are subclasses of OSError, which is a subclass of Exception. So isinstance returns True for all four ancestor classes.
For handler matching, Python checks except clauses top-to-bottom. except ConnectionError is checked first. Since NetworkTimeoutError is a subclass of ConnectionError, it matches immediately. The TimeoutError and OSError handlers are never reached.
Key insight: With multiple inheritance in exceptions, the object is an instance of ALL parent classes. But handler selection is purely top-to-bottom — whichever except clause matches first wins. If you swapped the order and put except TimeoutError first, that handler would catch it instead. The MRO determines isinstance truth, but handler selection depends on source order.
Expected Output
NetworkTimeoutError\nTrue\nTrue\nTrue\nTrue\nConnectionError handlerHints
Hint 1: Python uses C3 linearization (MRO) to determine the inheritance order for custom exceptions.
Hint 2: except clauses are checked top-to-bottom. The first matching clause wins.
Predict the output. Use __mro__ to inspect the full inheritance chain of ConnectionRefusedError, then verify a hierarchy relationship.
for cls in ConnectionRefusedError.__mro__:
print(cls.__name__)
print("---")
# Is every ConnectionRefusedError also an OSError?
print(issubclass(ConnectionRefusedError, OSError))
# Is every OSError also a ConnectionRefusedError?
print(issubclass(OSError, ConnectionRefusedError))Solution
for cls in ConnectionRefusedError.__mro__:
print(cls.__name__)
print("---")
# Is every ConnectionRefusedError also an OSError?
print(issubclass(ConnectionRefusedError, OSError))
# Is every OSError also a ConnectionRefusedError?
print(issubclass(OSError, ConnectionRefusedError))
Output:
ConnectionRefusedError
ConnectionError
OSError
Exception
BaseException
object
---
True
False
How it works: __mro__ (Method Resolution Order) lists every class in the inheritance chain, from the class itself up to object. For ConnectionRefusedError, the chain is: ConnectionRefusedError -> ConnectionError -> OSError -> Exception -> BaseException -> object.
issubclass(ConnectionRefusedError, OSError) is True because OSError appears in ConnectionRefusedError's MRO. But issubclass(OSError, ConnectionRefusedError) is False — a parent is NOT a subclass of its child. The relationship is one-directional.
Key insight: The MRO is the definitive map for exception catching behavior. If class A appears in class B's MRO, then except A will catch instances of B. You can inspect __mro__ programmatically to understand exactly which handlers will match which exceptions — essential when debugging complex exception hierarchies in large codebases.
Expected Output
ConnectionRefusedError\nConnectionError\nOSError\nException\nBaseException\nobject\n---\nTrue\nFalseHints
Hint 1: Every class has a __mro__ attribute that lists the full method resolution order as a tuple of classes.
Hint 2: The MRO determines isinstance behavior — an object is an instance of every class in its MRO.
Complete the classify_error function that routes exceptions to the correct category string. The function must check from most specific to most general.
def classify_error(exc):
if isinstance(exc, FileNotFoundError):
return "FILE_NOT_FOUND"
elif isinstance(exc, ConnectionError):
return "CONNECTION"
elif isinstance(exc, OSError):
return "OS_OTHER"
elif isinstance(exc, LookupError):
return "LOOKUP"
else:
return "UNHANDLED"
# Test cases
print(classify_error(FileNotFoundError("gone")))
print(classify_error(ConnectionRefusedError("refused")))
print(classify_error(PermissionError("denied")))
print(classify_error(KeyError("missing")))
print(classify_error(ValueError("bad")))Solution
def classify_error(exc):
if isinstance(exc, FileNotFoundError):
return "FILE_NOT_FOUND"
elif isinstance(exc, ConnectionError):
return "CONNECTION"
elif isinstance(exc, OSError):
return "OS_OTHER"
elif isinstance(exc, LookupError):
return "LOOKUP"
else:
return "UNHANDLED"
# Test cases
print(classify_error(FileNotFoundError("gone")))
print(classify_error(ConnectionRefusedError("refused")))
print(classify_error(PermissionError("denied")))
print(classify_error(KeyError("missing")))
print(classify_error(ValueError("bad")))
Output:
FILE_NOT_FOUND
CONNECTION
OS_OTHER
LOOKUP
UNHANDLED
How it works:
-
FileNotFoundError("gone")— matchesisinstance(exc, FileNotFoundError)directly. Even though it is also anOSError, the more specific check comes first. -
ConnectionRefusedError("refused")— does NOT matchFileNotFoundError. Matchesisinstance(exc, ConnectionError)becauseConnectionRefusedErroris a subclass ofConnectionError. -
PermissionError("denied")— does NOT matchFileNotFoundErrororConnectionError. Matchesisinstance(exc, OSError)becausePermissionErroris a subclass ofOSError. -
KeyError("missing")— does NOT match anyOSErrorcheck. Matchesisinstance(exc, LookupError)becauseKeyErroris a subclass ofLookupError. -
ValueError("bad")— does not match any of the specific checks. Falls through to"UNHANDLED".
Key insight: This pattern mirrors how except clauses work but in function form. The critical rule is the same: check from most specific to most general. If you moved the isinstance(exc, OSError) check before FileNotFoundError and ConnectionError, those specific categories would never be reached because both are subclasses of OSError. This is the isinstance equivalent of the "unreachable except clause" anti-pattern.
Expected Output
FILE_NOT_FOUND\nCONNECTION\nOS_OTHER\nLOOKUP\nUNHANDLEDHints
Hint 1: Use isinstance checks ordered from most specific to most general to route exceptions correctly.
Hint 2: The order of isinstance checks matters — check children before parents to avoid premature matching.
