Exception types¶
In this tutorial, you will explore the built-in exception hierarchy in Python. You will learn how Python organises exceptions into a class hierarchy, how to handle specific exception types, and how to use the Exception base class.
Time commitment: 15–20 minutes
Prerequisites:
- Completion of Your first exception
- Basic understanding of
try/exceptblocks
Learning objectives¶
By the end of this tutorial, you will be able to:
- List the most common built-in exception types and when they occur
- Explain the exception class hierarchy in Python
- Handle multiple exception types in a single
try/exceptblock - Use the
Exceptionbase class to handle broad categories of exceptions
Common built-in exceptions¶
Python provides many built-in exception types, each representing a specific kind of error. Here are the ones you will encounter most often:
| Exception | When it is raised |
|---|---|
ValueError |
A function receives a value of the correct type but an inappropriate value |
TypeError |
An operation is applied to an object of an inappropriate type |
KeyError |
A dictionary key is not found |
IndexError |
A sequence index is out of range |
FileNotFoundError |
A file or directory is requested but does not exist |
ZeroDivisionError |
Division or modulo by zero |
AttributeError |
An attribute reference or assignment fails |
NameError |
A local or global name is not found |
Let us see each of these in action.
# ValueError: inappropriate value for the type
try:
number = int("hello")
except ValueError as e:
print(f"ValueError: {e}")
# TypeError: inappropriate type for the operation
try:
result = "10" + 5
except TypeError as e:
print(f"TypeError: {e}")
# KeyError: key not found in dictionary
try:
data = {"name": "Alice", "age": 30}
email = data["email"]
except KeyError as e:
print(f"KeyError: {e}")
# IndexError: list index out of range
try:
numbers = [1, 2, 3]
value = numbers[10]
except IndexError as e:
print(f"IndexError: {e}")
The exception hierarchy¶
All built-in exceptions in Python are organised into a class hierarchy. At the very top is BaseException, and most exceptions you will handle inherit from Exception.
Here is a simplified view of the hierarchy:
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── IsADirectoryError
├── ValueError
├── TypeError
├── AttributeError
└── NameError
This hierarchy matters because when you handle a parent exception, you also handle all its children.
Handling parent exceptions¶
Because ZeroDivisionError inherits from ArithmeticError, you can handle it using either type.
# Handling the parent class catches the child exception
try:
result = 10 / 0
except ArithmeticError as e:
print(f"Caught an ArithmeticError: {e}")
print(f"Actual type: {type(e).__name__}")
Similarly, IndexError and KeyError both inherit from LookupError.
# LookupError handles both IndexError and KeyError
def safe_lookup(collection, key):
"""Safely look up a value in a list or dictionary."""
try:
return collection[key]
except LookupError as e:
print(f"Lookup failed ({type(e).__name__}): {e}")
return None
# Works with lists (IndexError)
safe_lookup([1, 2, 3], 10)
# Works with dictionaries (KeyError)
safe_lookup({"a": 1}, "z")
Handling multiple exception types¶
You can handle different exception types in separate except clauses. Python checks each clause in order and runs the first one that matches.
def convert_and_divide(value: str, divisor: float) -> float | None:
"""Convert a string to a number and divide it by the divisor."""
try:
number = float(value)
return number / divisor
except ValueError:
print(f"Cannot convert {value!r} to a number.")
return None
except ZeroDivisionError:
print("Cannot divide by zero.")
return None
print(convert_and_divide("10", 3)) # Normal operation
print(convert_and_divide("hello", 3)) # Triggers ValueError
print(convert_and_divide("10", 0)) # Triggers ZeroDivisionError
Handling multiple exceptions in one clause¶
If you want to handle several exception types in the same way, you can list them in a tuple.
def parse_number(value: str) -> float | None:
"""Parse a string to a number, handling both ValueError and TypeError."""
try:
return float(value)
except (ValueError, TypeError) as e:
print(f"Could not parse {value!r}: {e}")
return None
print(parse_number("3.14")) # Valid float string
print(parse_number("abc")) # Triggers ValueError
print(parse_number(None)) # Triggers TypeError
Checking the exception hierarchy with issubclass()¶
You can use issubclass() to verify the relationships between exception types.
# ZeroDivisionError is a subclass of ArithmeticError
print(issubclass(ZeroDivisionError, ArithmeticError))
# ArithmeticError is a subclass of Exception
print(issubclass(ArithmeticError, Exception))
# FileNotFoundError is a subclass of OSError
print(issubclass(FileNotFoundError, OSError))
# KeyError is a subclass of LookupError
print(issubclass(KeyError, LookupError))
Why you should handle specific exceptions¶
It can be tempting to handle Exception (or worse, use a bare except) to deal with all possible errors at once. However, this is generally a bad practice because it can hide bugs in your code.
Consider the following example.
# Bad practice: catching all exceptions hides bugs
def bad_divide(a: float, b: float) -> float | None:
"""A poorly written divide function that hides bugs."""
try:
return a / b
except Exception:
# This hides ALL errors, including ones you did not expect
return None
# This hides a TypeError that indicates a bug in your code
print(bad_divide("ten", 2)) # Returns None instead of revealing the bug
# Good practice: handle only the exceptions you expect
def good_divide(a: float, b: float) -> float | None:
"""A well-written divide function that handles only expected exceptions."""
try:
return a / b
except ZeroDivisionError:
return None
# This correctly reveals the bug as a TypeError
try:
good_divide("ten", 2)
except TypeError as e:
print(f"Bug revealed: {e}")
By handling only ZeroDivisionError, the TypeError propagates up and reveals the bug. This makes debugging much easier.
Exercises¶
Exercise 1: Identify the exception¶
Write a function called identify_exception that takes a callable (a function) and calls it inside a try/except block. It should handle ValueError, TypeError, KeyError, and IndexError, and return a string naming which exception was raised. If no exception occurs, return "no exception".
from collections.abc import Callable
def identify_exception(func: Callable) -> str:
"""Call the function and return the name of any exception raised."""
pass # Replace this with your implementation
Click to reveal the solution
from collections.abc import Callable
def identify_exception(func: Callable) -> str:
"""Call the function and return the name of any exception raised."""
try:
func()
return "no exception"
except ValueError:
return "ValueError"
except TypeError:
return "TypeError"
except KeyError:
return "KeyError"
except IndexError:
return "IndexError"
Exercise 2: Using parent exceptions¶
Write a function called safe_access that takes a collection (list or dictionary) and a key, and returns the value. Use LookupError to handle both IndexError and KeyError in a single except clause. Return None if the lookup fails.
from typing import Any
def safe_access(collection: list | dict, key: Any) -> Any:
"""Safely access a value from a list or dictionary."""
pass # Replace this with your implementation
Click to reveal the solution
from typing import Any
def safe_access(collection: list | dict, key: Any) -> Any:
"""Safely access a value from a list or dictionary."""
try:
return collection[key]
except LookupError:
return None
Summary¶
In this tutorial, you learned the following:
- Python provides many built-in exception types, each representing a specific kind of error
- Exceptions are organised in a class hierarchy, with
BaseExceptionat the top andExceptionas the parent of most exceptions you will handle - Handling a parent exception also handles all its child exceptions
- You can handle multiple exception types using separate
exceptclauses or by grouping them in a tuple - Always handle specific exceptions rather than using broad
exceptclauses, to avoid hiding bugs
In the next tutorial, Raising exceptions, you will learn how to raise your own exceptions and create custom exception classes.