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

Exception Trigger
ValueError Invalid value for a function or operation
TypeError Wrong data type used
ZeroDivisionError Division by zero
FileNotFoundError Missing file reference
IndexError Invalid list index
KeyError Missing key in dictionary

Basic Try-Except Block

# 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

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:

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:

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:

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

def add(a, b): return a + b
def subtract(a, b): return a - b
def multiply(a, b): return a * b

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero.")
    return a / b

def main():
    try:
        operation = input("Choose operation (add/subtract/multiply/divide): ").strip().lower()
        num1 = float(input("Enter first number: "))
        num2 = float(input("Enter second number: "))

        if operation == "add":
            result = add(num1, num2)
        elif operation == "subtract":
            result = subtract(num1, num2)
        elif operation == "multiply":
            result = multiply(num1, num2)
        elif operation == "divide":
            result = divide(num1, num2)
        else:
            return

        print(f"Result: {result}")

    except ValueError as ve:
        print(f"Value error: {ve}")
    except Exception as e:
        print("An unexpected error occurred.")

if __name__ == "__main__":
    main()

5.6 Unit Testing Expected Exceptions

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

test_calculator.py

import pytest
from calculator import add, subtract, multiply, divide

def test_add():
    assert add(1, 2) == 3

def test_subtract():
    assert subtract(5, 2) == 3

def test_multiply():
    assert multiply(3, 3) == 9

def test_divide():
    assert divide(6, 2) == 3

def test_divide_by_zero():
    with pytest.raises(ValueError, match="Cannot divide by zero."):
        divide(10, 0)

Check your understanding

Test your knowledge of Error Handling in Python