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
| Situation | Correct Exception |
|---|---|
| Wrong argument value | ValueError |
| Wrong argument type | TypeError |
| Non-existent dictionary key | KeyError |
| List/tuple index out of range | IndexError |
| File not found | FileNotFoundError |
| No permission | PermissionError |
| Unimplemented feature | NotImplementedError |
| Instantiating abstract base class directly | TypeError |
| Recursion depth exceeded | RecursionError |
| Module not found | ModuleNotFoundError |
| General runtime error | RuntimeError |
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