Skip to main content

Python Exception Hierarchy Practice Problems & Exercises

Practice: Exception Hierarchy

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Parent Catches ChildEasy
hierarchyparent-childOSError

Predict the output. An except OSError clause encounters a FileNotFoundError. Determine what gets caught and what isinstance reports.

Python
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\nTrue
Hints

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.

#2BaseException vs ExceptionEasy
BaseExceptionExceptionSystemExitKeyboardInterrupt

Predict the output. Check where SystemExit and ValueError sit in the hierarchy using issubclass.

Python
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: True
Hints

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.

#3LookupError Catches Both IndexError and KeyErrorEasy
LookupErrorIndexErrorKeyErrorhierarchy

Predict the output. A single except LookupError clause handles errors from both a list and a dictionary.

Python
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 LookupError
Hints

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.

#4ArithmeticError Catches ZeroDivisionError and OverflowErrorEasy
ArithmeticErrorZeroDivisionErrorOverflowErrorhierarchy

Predict the output. Two different math errors are both caught by except ArithmeticError.

Python
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\nTrue
Hints

Hint 1: ZeroDivisionError and OverflowError are both subclasses of ArithmeticError.

Hint 2: Python ints never overflow — OverflowError only affects floats.


Medium

#5Handler Order Matters: Child Before ParentMedium
handler-orderspecificityMRO

Predict the output. Three different handler orderings produce different results. Determine which handler catches each exception.

Python
# 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:

  1. Case 1: Python checks except FileNotFoundError first. The raised FileNotFoundError matches, so this handler runs. The except OSError clause is never reached.

  2. Case 2: Python checks except OSError first. Since FileNotFoundError is a subclass of OSError, it matches. The except FileNotFoundError clause below is unreachable — it can never execute for any OSError subclass.

  3. Case 3: Only FileNotFoundError is caught. If a PermissionError were 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 handler
Hints

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.

#6isinstance Checks Through the HierarchyMedium
isinstancehierarchyMROinspection

Predict the output. Check how isinstance traverses the full inheritance chain for a ConnectionRefusedError.

Python
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\nFalse
Hints

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.

#7OSError Consolidation: Old Names Are AliasesMedium
OSErrorIOErroraliasesconsolidation

Predict the output. Verify that the pre-3.3 exception names are now aliases for OSError.

Python
# 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\nTrue
Hints

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.

#8Catching Exception Does Not Catch SystemExitMedium
BaseExceptionExceptionSystemExitsafety

Predict the output. Compare what except Exception and except BaseException catch when SystemExit is raised.

Python
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:

  1. ValueError is a subclass of Exception, so except Exception catches it.

  2. sys.exit(1) raises SystemExit, which inherits from BaseException directly — NOT from Exception. The inner except Exception does not match, so SystemExit propagates to the outer except SystemExit.

  3. except BaseException catches everything, including SystemExit. This is why except BaseException (and bare except:) 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 BaseException
Hints

Hint 1: except Exception catches all program errors but NOT SystemExit, KeyboardInterrupt, or GeneratorExit.

Hint 2: except BaseException catches everything, including termination signals.


Hard

#9MRO-Based Handler Resolution with Multiple InheritanceHard
MROmultiple-inheritancecustom-exceptionshandler-resolution

Predict the output. A custom exception inherits from both ConnectionError and TimeoutError. Determine how isinstance and handler matching work.

Python
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 handler
Hints

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.

#10Walking the Exception MRO ProgrammaticallyHard
MROinspection__mro__hierarchy-navigation

Predict the output. Use __mro__ to inspect the full inheritance chain of ConnectionRefusedError, then verify a hierarchy relationship.

Python
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\nFalse
Hints

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.

#11Building a Hierarchy-Aware Exception RouterHard
hierarchyisinstancedispatchreal-world

Complete the classify_error function that routes exceptions to the correct category string. The function must check from most specific to most general.

Python
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:

  1. FileNotFoundError("gone") — matches isinstance(exc, FileNotFoundError) directly. Even though it is also an OSError, the more specific check comes first.

  2. ConnectionRefusedError("refused") — does NOT match FileNotFoundError. Matches isinstance(exc, ConnectionError) because ConnectionRefusedError is a subclass of ConnectionError.

  3. PermissionError("denied") — does NOT match FileNotFoundError or ConnectionError. Matches isinstance(exc, OSError) because PermissionError is a subclass of OSError.

  4. KeyError("missing") — does NOT match any OSError check. Matches isinstance(exc, LookupError) because KeyError is a subclass of LookupError.

  5. 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\nUNHANDLED
Hints

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.

© 2026 EngineersOfAI. All rights reserved.