Skip to main content
Advertisement

Built-in Exception Hierarchy — Exception Tree and Choosing the Right Exception

Python provides dozens of built-in exception classes. These exceptions are organized in a hierarchy, and choosing the correct exception is the first step toward robust code.


BaseException Hierarchy Tree

BaseException
├── SystemExit # When sys.exit() is called
├── KeyboardInterrupt # When Ctrl+C is pressed
├── GeneratorExit # When generator .close() is called
└── Exception # Base class for program errors
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── AttributeError # Accessing a non-existent attribute
├── EOFError # input() reaches EOF
├── ImportError
│ └── ModuleNotFoundError
├── LookupError
│ ├── IndexError # List index out of range
│ └── KeyError # Dictionary key not found
├── MemoryError
├── NameError
│ └── UnboundLocalError
├── OSError # Base for file/IO errors
│ ├── FileNotFoundError
│ ├── FileExistsError
│ ├── PermissionError
│ ├── TimeoutError
│ └── IsADirectoryError
├── RuntimeError
│ ├── RecursionError
│ └── NotImplementedError
├── StopIteration # Iterator exhausted
├── TypeError # Wrong type
├── ValueError # Right type but wrong value
│ └── UnicodeError
│ ├── UnicodeDecodeError
│ └── UnicodeEncodeError
└── Warning # Base class for warnings
├── DeprecationWarning
├── RuntimeWarning
└── UserWarning

Exception vs BaseException

Never catch BaseException directly.

# Bad: also catches SystemExit, KeyboardInterrupt
try:
do_something()
except BaseException:
pass # Ctrl+C won't exit the program!

# Correct: only handle program errors
try:
do_something()
except Exception:
pass

# Only catch Ctrl+C and sys.exit() when you intend to
try:
long_running_task()
except KeyboardInterrupt:
print("User interrupted. Cleaning up...")
cleanup()

SystemExit and KeyboardInterrupt are signals related to program control and should be excluded from normal exception handling.


Key Built-in Exceptions in Detail

ValueError — Correct type but wrong value

# Common scenarios
int("abc") # ValueError: invalid literal
int("3.14") # ValueError: invalid literal (decimal string)
[].pop() # ValueError: pop from empty list (not IndexError!)
"hello".index("z") # ValueError: substring not found

# Proper handling
def parse_age(value: str) -> int:
try:
age = int(value)
if age < 0 or age > 150:
raise ValueError(f"Age out of range: {age}")
return age
except ValueError as e:
print(f"Invalid age: {e}")
return 0

TypeError — Wrong type used

# Common scenarios
"hello" + 5 # TypeError: can only concatenate str
len(42) # TypeError: object of type 'int' has no len()
None() # TypeError: 'NoneType' is not callable

# Prevention via type checking
def add_numbers(a, b):
if not isinstance(a, (int, float)):
raise TypeError(f"Numeric type required, got: {type(a).__name__}")
return a + b

KeyError — Dictionary key not found

# Common scenario
data = {"name": "Alice"}
data["age"] # KeyError: 'age'

# Correct handling approaches
# Method 1: get() method
age = data.get("age", 0) # Returns default value

# Method 2: in operator
if "age" in data:
age = data["age"]

# Method 3: try-except
try:
age = data["age"]
except KeyError:
age = 0

# Method 4: defaultdict
from collections import defaultdict
counts = defaultdict(int)
counts["a"] += 1 # No KeyError

IndexError — Sequence index out of range

# Common scenario
items = [1, 2, 3]
items[5] # IndexError: list index out of range
items[-10] # IndexError: list index out of range

# Safe access pattern
def safe_get(lst: list, index: int, default=None):
try:
return lst[index]
except IndexError:
return default

# Or use a conditional check
if 0 <= index < len(items):
value = items[index]

AttributeError — Non-existent attribute

# Common scenarios
x = None
x.name # AttributeError: 'NoneType' has no attribute 'name'
"hello".upper() # This is fine — upper method exists
(1, 2).append(3) # AttributeError: 'tuple' has no attribute 'append'

# Safe attribute access
name = getattr(obj, "name", "unknown") # Use default value
hasattr(obj, "name") # Check existence

OSError / FileNotFoundError — File and I/O errors

# OSError hierarchy: FileNotFoundError, PermissionError, etc.
import os

def safe_read(path: str) -> str:
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
print(f"File not found: {path}")
except PermissionError:
print(f"No permission: {path}")
except IsADirectoryError:
print(f"Is a directory: {path}")
except OSError as e:
# OS-related errors not caught above
print(f"IO error ({e.errno}): {e.strerror}")
return ""

You can check the OS-level error code via the errno attribute.

import errno

try:
open("/nonexistent/path/file.txt")
except OSError as e:
if e.errno == errno.ENOENT:
print("File not found")
elif e.errno == errno.EACCES:
print("Access denied")

Catching Multiple Exceptions — except (A, B)

Listing exception types in a tuple inside parentheses lets you handle multiple exceptions with one except clause.

def parse_input(value: str) -> int:
try:
return int(value)
except (ValueError, TypeError):
# ValueError: non-numeric string
# TypeError: None or other type that can't be passed to int()
return 0

# Multiple OS errors in one clause
def read_data(path: str) -> bytes:
try:
with open(path, "rb") as f:
return f.read()
except (FileNotFoundError, IsADirectoryError):
return b""
except (PermissionError, OSError) as e:
print(f"Read failed: {e}")
return b""

Note: In except (A, B) as e, e is the raised exception instance. Both exception types are handled but to distinguish which type was raised, use isinstance(e, A).


Guide to Choosing the Right Exception

SituationCorrect Exception
Wrong argument valueValueError
Wrong argument typeTypeError
Non-existent dictionary keyKeyError
List/tuple index out of rangeIndexError
File not foundFileNotFoundError
No permissionPermissionError
Unimplemented featureNotImplementedError
Instantiating abstract base class directlyTypeError
Recursion depth exceededRecursionError
Module not foundModuleNotFoundError
General runtime errorRuntimeError

except Exception vs bare except

# bare except — absolutely forbidden
try:
risky()
except: # Includes SystemExit, KeyboardInterrupt!
pass

# except Exception — acceptable. Catches only program errors
try:
risky()
except Exception:
pass

# Naming specific exceptions — most recommended
try:
risky()
except (ValueError, RuntimeError) as e:
handle(e)

A bare except behaves the same as except BaseException. It catches Ctrl+C (KeyboardInterrupt) and sys.exit() (SystemExit), which can create bugs where the program refuses to exit as expected.


Practical Example — Branching Based on Exception Type

from typing import Any
import json
import os

def load_json_file(path: str) -> dict[str, Any]:
"""
Reads a JSON file and provides appropriate exception messages.
"""
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except FileNotFoundError:
raise FileNotFoundError(
f"Config file not found: {path}\n"
f"Current directory: {os.getcwd()}"
)
except PermissionError:
raise PermissionError(
f"No permission to read file: {path}"
)
except json.JSONDecodeError as e:
raise ValueError(
f"JSON parse failure ({path}:{e.lineno}:{e.colno}): {e.msg}"
) from e
except UnicodeDecodeError as e:
raise ValueError(
f"File encoding error ({path}): Not a UTF-8 file"
) from e

Expert Tips

Tip 1 — Using LookupError for unified handling

IndexError and KeyError are both subclasses of LookupError. Use it when the type of container is uncertain.

def safe_lookup(container, key):
try:
return container[key]
except LookupError:
return None

Tip 2 — Using ArithmeticError for unified math errors

def safe_math(a, b, op):
try:
if op == "/":
return a / b
elif op == "**":
return a ** b
except ArithmeticError as e:
# Handles both ZeroDivisionError and OverflowError
print(f"Math error: {type(e).__name__}: {e}")
return None

Tip 3 — Exploring the exception hierarchy directly

# Check MRO (Method Resolution Order) of exceptions
print(FileNotFoundError.__mro__)
# (<class 'FileNotFoundError'>, <class 'OSError'>,
# <class 'Exception'>, <class 'BaseException'>, <class 'object'>)

# Check if one exception is a subclass of another
issubclass(FileNotFoundError, OSError) # True
issubclass(KeyError, LookupError) # True

Tip 4 — Python 3.11+ Exception Notes

In Python 3.11+, you can attach additional notes to exceptions using the add_note() method.

try:
process_data(raw_data)
except ValueError as e:
e.add_note(f"Raw data: {raw_data!r}")
e.add_note("Please check the data format documentation.")
raise
Advertisement