Built-in Exceptions
TL;DR
Python’s built-in exceptions form a strict inheritance hierarchy rooted in BaseException, with all user code exceptions inheriting from Exception. Choose specific exception types like ValueError (wrong value) or TypeError (wrong type) rather than generic exceptions, and consider carefully before catching BaseException in user code as it will do things like capture keyboard interrupts (no Ctrl-C for you!).
Interesting!
Python automatically tracks exception context, storing both the exception being handled (__context__) and any explicitly chained exception (__cause__). This creates a complete audit trail showing how one error led to another, making debugging multi-layered failures much easier.
Understanding the Exception Hierarchy
The exception hierarchy determines which exceptions get caught by which except clauses:
python code snippet start
BaseException
├── SystemExit # Don't catch these!
├── KeyboardInterrupt # Don't catch these!
└── Exception # Derive your exceptions from here
├── ValueError
├── TypeError
├── KeyError
└── ... many morepython code snippet end
When creating custom exceptions, always inherit from Exception, not BaseException (unless you really want to capture those base exceptions):
python code snippet start
class ValidationError(Exception):
passpython code snippet end
Choosing the Right Exception
Python provides specific exceptions for specific situations. Using the right one makes your code clearer:
python code snippet start
# TypeError: wrong type entirely
def greet(name: str):
if not isinstance(name, str):
raise TypeError(f"Expected str, got {type(name).__name__}")
return f"Hello, {name}"
# ValueError: right type, wrong value
def validate_age(age: int):
if age < 0 or age > 150:
raise ValueError(f"Age must be 0-150, got {age}")
return age
# Or using a custom exception
class AgeRangeError(ValueError):
pass
def validate_age_customex(age: int):
if age < 0 or age > 150:
raise AgeRangeError(f"Age must be 0-150, got {age}")
return age
# KeyError: missing dictionary key
settings = {"timeout": 30}
settings["retries"] # Raises KeyError
# AttributeError: missing attribute
class Person:
pass
person = Person()
person.name # Raises AttributeErrorpython code snippet end
The most commonly used exceptions are ValueError, TypeError, KeyError, IndexError, AttributeError, and FileNotFoundError.
OS Exception Consolidation
Python 3.3 consolidated OS-related exceptions under OSError with specific subclasses mapped to error codes:
python code snippet start
# These are all OSError subclasses now
try:
open("/nonexistent/path.txt")
except FileNotFoundError: # errno.ENOENT
print("File not found")
try:
open("/root/protected.txt")
except PermissionError: # errno.EACCES
print("Permission denied")
try:
os.mkdir("existing_dir")
except FileExistsError: # errno.EEXIST
print("Already exists")python code snippet end
This makes exception handling much more specific than the old approach of catching OSError and checking errno.
Exception Chaining and Context
Python tracks the relationship between exceptions automatically:
python code snippet start
# Implicit context
try:
data = json.loads(file.read())
except JSONDecodeError as e:
# e.__context__ is automatically set to any exception
# that was active when this except block ran
raise ValueError("Invalid config file")
# Explicit chaining with 'from'
try:
result = parse_input(user_data)
except ValueError as e:
raise ConfigError("Invalid configuration") from e
# Sets __cause__ and suppresses __context__ in tracebackpython code snippet end
Explicit chaining with from makes the cause-and-effect relationship clear in the traceback.
Exception Groups (Python 3.11+)
Exception groups handle multiple unrelated exceptions together, useful for concurrent code:
python code snippet start
def process_batch(items):
errors = []
for item in items:
try:
validate(item)
except Exception as e:
errors.append(e)
if errors:
raise ExceptionGroup("Validation failed", errors)
# Catch with except*
try:
process_batch(data)
except* ValueError as eg:
print(f"Values: {eg.exceptions}")
except* TypeError as eg:
print(f"Types: {eg.exceptions}")python code snippet end
The except* syntax allows handling different exception types from the group separately.
Adding Notes to Exceptions (Python 3.11+)
You can add contextual information to exceptions before re-raising them:
python code snippet start
def load_config(filename):
try:
with open(filename) as f:
return json.load(f)
except FileNotFoundError as e:
e.add_note(f"Config file '{filename}' is required")
e.add_note("Run setup.py to create default config")
raise
except json.JSONDecodeError as e:
e.add_note(f"Invalid JSON in {filename}")
e.add_note("Check for trailing commas or quotes")
raisepython code snippet end
Notes appear in the traceback, providing helpful debugging context without changing the exception type.
NotImplementedError for Abstract Methods
Use NotImplementedError to mark methods that subclasses must override:
python code snippet start
class BaseProcessor:
def process(self, data):
raise NotImplementedError(
"Subclasses must implement process()"
)
class CSVProcessor(BaseProcessor):
def process(self, data):
# Actual implementation
return data.split(',')python code snippet end
Don’t confuse this with NotImplemented, which is a constant used for comparison operators, not an exception.
Python’s exception hierarchy provides rich semantics for error handling. Using the right exception type makes your code self-documenting and helps callers handle errors appropriately.
Errors and Exceptions tutorial covers exception handling in detail, while PEP 3134 explains the exception chaining mechanism.
Reference: Built-in Exceptions — Python Documentation