Raising Exceptions in Python

Use examples when they help make things clearer.

Let me break down how raising exceptions works in Python using a more conversational tone:

So you want to learn how to raise exceptions in Python?

To kick things off, let’s define what an exception is. In programming terms, it’s basically when something goes wrong and your code can no longer continue running as expected. This could be anything from trying to access a nonexistent index in a list or attempting to divide by zero (which is always a bad idea).

When this happens, Python automatically raises an exception for you. But what if you want more control over how errors are handled? That’s where the raise statement comes in handy! This keyword allows you to manually trigger exceptions and handle them on your own terms.

Here’s a simple example:

# Define a function that raises an exception when called
def divide_by_zero():
    # Calculate the result of dividing two numbers (42 and 0)
    result = 42 / 0 # This line will cause an error because you cannot divide by zero
    
    # Raise a ValueError exception with a custom error message
    raise ValueError("You can't divide by zero, dummy!") # This line will manually trigger an exception and provide a custom error message

# The following code will call the function and handle the exception
try:
    divide_by_zero() # This line will call the function and trigger the exception
except ValueError as err: # This line will catch the exception and assign it to the variable "err"
    print(err) # This line will print the custom error message from the exception

In this example, we define a function called `divide_by_zero()`. This function calculates the result of dividing 42 by 0 (which is obviously not possible). When Python tries to execute that line of code, it automatically raises a ZeroDivisionError exception. However instead of letting Python handle this error for us, we use the `raise` keyword to manually trigger an exception and provide our own custom error message.

Now let’s say you want to raise your own custom exceptions in Python. This can be useful when you need to create project-specific errors or exceptional situations that don’t have a built-in equivalent. To do this, simply inherit from the `Exception` class and add any additional functionality as needed:

# Define a custom exception called GradeValueError for our gradebook app
class GradeValueError(Exception): # defining a new class called GradeValueError that inherits from the Exception class
    # Add some extra information to the error message using string formatting
    def __init__(self, value): # defining a constructor method that takes in a value parameter
        self.value = value # assigning the value parameter to the instance variable "value"
        
        # Call the parent constructor (which is required when inheriting from a class)
        super().__init__(f"Invalid grade value: {self.value}") # calling the constructor of the parent class and passing in a formatted string as the error message, which includes the value that caused the error

In this example, we define a custom exception called `GradeValueError`. This exception will be used to handle situations where a student’s grade falls outside the range of 0-100 (which is not allowed in our gradebook app). To create an instance of this error, simply call it like so:

# Define a custom exception called GradeValueError
class GradeValueError(Exception):
    # Initialize the exception with a message and a grade value
    def __init__(self, grade):
        # Call the parent class's __init__ method and pass in the message
        super().__init__("Grade value must be between 0 and 100.")
        # Store the grade value in an attribute
        self.grade = grade

# Create an instance of the GradeValueError exception with a custom grade value of 95
raise GradeValueError(95)

And that’s all there is to raising exceptions in Python! By using the `raise` keyword and defining your own custom errors, you can have more control over how errors are handled in your code.

But what if you want to handle an exception that has already been raised? That’s where the try-except block comes in handy:

# Define a function that calculates the square root of a number using recursion
def calculate_square_root(num):
    # Check for invalid input (negative numbers) and raise an exception if necessary
    if num < 0:
        raise ValueError("Cannot take the square root of negative numbers") # raise ValueError if input is negative
    
    # Calculate the result using recursion
    def sqrt(x, guess=1.0):
        while True:
            next_guess = (guess + x / guess) * 0.5 # calculate the next guess using the formula for square root
            if abs(next_guess - guess) < 1e-6: # check if the difference between the next guess and current guess is within a small margin of error
                return next_guess # return the next guess as the square root
            guess = next_guess # update the current guess with the next guess
    
    # Call the recursive function and handle any exceptions that are raised
    try:
        result = sqrt(num) # call the sqrt function with the input number
        
        # Print out the calculated square root value
        print("The square root of", num, "is:", result)
    except ValueError as e:
        # Catch the exception and print out a custom error message
        print("Error:", str(e)) # print the custom error message if a ValueError is raised

In this example, we define a function called `calculate_square_root()`. This function uses recursion to calculate the square root of a number. However, before doing so, it checks for invalid input (negative numbers) and raises an exception if necessary using the try-except block. If an exception is raised within this block, Python will jump to the except clause instead of continuing with the rest of the code in the function.

The `try` keyword tells Python that we want to execute a block of code and potentially handle any exceptions that are raised during its execution. The `except` keyword specifies what type of exception we’re expecting (in this case, a ValueError) and how we want to handle it (by printing out an error message).

By using try-except blocks in your Python code, you can catch specific types of exceptions and provide custom handling for them. This can be useful when dealing with complex or unpredictable input data that might cause unexpected errors.

SICORPS