Error Handling in Python

Chapter Outline

Chapter 5: Error Handling in Python

Bugs are inevitable—but crashes don’t have to be. This chapter explores how to handle errors gracefully.

5.1 Python Exceptions

Python provides a rich set of built-in exceptions that help developers gracefully handle unexpected conditions.

Common Built-in Exceptions

ExceptionTrigger
ValueErrorInvalid value for a function or operation
TypeErrorWrong data type used
ZeroDivisionErrorDivision by zero
FileNotFoundErrorMissing file reference
IndexErrorInvalid list index
KeyErrorMissing key in dictionary

Basic Try-Except Block

python
# ValueError
try:
x = int("abc")
except ValueError as e:
print("ValueError:", e)
# TypeError
try:
length = len(42)
except TypeError as e:
print("TypeError:", e)
# ZeroDivisionError
try:
result = 10 / 0
except ZeroDivisionError as e:
print("ZeroDivisionError:", e)
# FileNotFoundError
try:
with open("missing.txt", "r") as f:
content = f.read()
except FileNotFoundError as e:
print("FileNotFoundError:", e)
# IndexError
try:
nums = [1, 2, 3]
print(nums[5])
except IndexError as e:
print("IndexError:", e)
# KeyError
try:
person = {"name": "Alice"}
print(person["age"])
except KeyError as e:
print("KeyError:", e)

5.2 Raising Custom Exceptions

In Python, you can create your own custom exceptions by defining a new class that inherits from Python's built-in Exception class (or any of its subclasses). This allows you to raise and catch errors that are specific to your application's domain logic.

We talk about subclasses in details in the next chapter.

Example: Custom Exception

python
import math
class NegativeInputError(Exception):
"""Raised when a negative number is provided where not allowed."""
pass
def calculate_square_root(x):
if x < 0:
raise NegativeInputError("Cannot compute square root of a negative number.")
return math.sqrt(x)
  • NegativeInputError is your custom exception.
  • It inherits from Python's built-in Exception.
  • The pass keyword here is a placeholder indicating no additional logic is being added yet. The pass statement in Python is a no-op — it does nothing when executed.
  • The raise keyword is used to raise an exception of a specific type, be they built-in or custom.

Output:

bash
Traceback (most recent call last):
File "/.../custom_exception.py", line 16, in <module>
rt2 = calculate_square_root(-5)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "/.../custom_exception.py", line 10, in calculate_square_root
raise NegativeInputError("Cannot compute square root of a negative number.")
NegativeInputError: Cannot compute square root of a negative number.

A stack trace similar to the above is produced when the earlier code is executed.

Adding More to Your Custom Exception

You can also customize your exception further by overriding the __init__ or __str__ methods:

python
class ValidationError(Exception):
def __init__(self, field, message):
self.field = field
self.message = message
super().__init__(f"{field}: {message}")
raise ValidationError("username", "Must not be empty")

Output:

bash
ValidationError: username: Must not be empty

5.5 Refactoring the Calculator: Fault-Tolerant Version

Here’s a refactored CLI calculator with logging and error handling.

calculator.py
1def add(a, b): return a + b
2def subtract(a, b): return a - b
3def multiply(a, b): return a * b
4
5def divide(a, b):
6 if b == 0:
7 raise ValueError("Cannot divide by zero.")
8 return a / b
9
10def main():
11 try:
12 operation = input("Choose operation (add/subtract/multiply/divide): ").strip().lower()
13 num1 = float(input("Enter first number: "))
14 num2 = float(input("Enter second number: "))
15
16 if operation == "add":
17 result = add(num1, num2)
18 elif operation == "subtract":
19 result = subtract(num1, num2)
20 elif operation == "multiply":
21 result = multiply(num1, num2)
22 elif operation == "divide":
23 result = divide(num1, num2)
24 else:
25 return
26
27 print(f"Result: {result}")
28
29 except ValueError as ve:
30 print(f"Value error: {ve}")
31 except Exception as e:
32 print("An unexpected error occurred.")
33
34if __name__ == "__main__":
35 main()

5.6 Unit Testing Expected Exceptions

We’ll now test for normal results and for expected failures using pytest.

test_calculator.py
1import pytest
2from calculator import add, subtract, multiply, divide
3
4def test_add():
5 assert add(1, 2) == 3
6
7def test_subtract():
8 assert subtract(5, 2) == 3
9
10def test_multiply():
11 assert multiply(3, 3) == 9
12
13def test_divide():
14 assert divide(6, 2) == 3
15
16def test_divide_by_zero():
17 with pytest.raises(ValueError, match="Cannot divide by zero."):
18 divide(10, 0)

Check your understanding

Test your knowledge of Error Handling in Python

Feedback