Python - Exception Handling

5.
Discuss the concept of raising exceptions in Python.

Raising Exceptions in Python:

In Python, exceptions can be raised explicitly using the raise statement. This allows you to signal that an error or exceptional condition has occurred at a specific point in your code. You can raise built-in exceptions or create custom exceptions to handle specific cases.

Here is an example program demonstrating the concept of raising exceptions:

def divide_numbers(num1, num2):
    if num2 == 0:
        raise ValueError("Cannot divide by zero.")
    else:
        return num1 / num2

try:
    result = divide_numbers(10, 0)

except ValueError as e:
    print("Error:", e)

else:
    print("Result:", result)

Output (Example):

Error: Cannot divide by zero.

In this example, the divide_numbers function raises a ValueError if the denominator (num2) is zero. The try and except blocks handle this raised exception, preventing the program from crashing and allowing you to handle the error gracefully.

Raising exceptions is useful when you want to indicate that a specific condition, which violates the normal flow of the program, has occurred. It allows you to handle errors in a structured manner and provide meaningful error messages.


6.
How can you catch multiple exceptions in a single except block?

Catching Multiple Exceptions in a Single Except Block:

In Python, you can catch multiple exceptions in a single except block by specifying the exceptions as a tuple. This allows you to handle different types of exceptions with a common set of statements. It's a convenient way to provide a unified error-handling strategy for multiple exception scenarios.

Here is an example program demonstrating the concept of catching multiple exceptions:

def perform_division(num1, num2):
    try:
        result = num1 / num2

    except (ZeroDivisionError, TypeError) as e:
        print(f"Error: {e}")
        result = None

    return result

# Example usage
result1 = perform_division(10, 2)
result2 = perform_division(10, 0)
result3 = perform_division(10, "2")

print("Result 1:", result1)
print("Result 2:", result2)
print("Result 3:", result3)

Output (Example):

Result 1: 5.0
Error: division by zero
Result 2: None
Error: unsupported operand type(s) for /: 'int' and 'str'
Result 3: None

In this example, the perform_division function attempts to perform division and catches both ZeroDivisionError and TypeError exceptions. The common error-handling code is executed for both types of exceptions, providing a consistent response to different exceptional conditions.


7.
Explain the purpose of the finally block in exception handling.

Purpose of the Finally Block in Exception Handling:

The finally block in exception handling is used to define a set of statements that will be executed regardless of whether an exception is raised or not. It provides a way to ensure that certain code is always executed, such as cleanup operations or resource releases, even if an exception occurs within the try block.

Here is an example program demonstrating the use of the finally block:

def perform_division(num1, num2):
    try:
        result = num1 / num2

    except ZeroDivisionError as e:
        print(f"Error: {e}")
        result = None

    finally:
        print("This block always gets executed.")
        # Perform cleanup or release resources here (if needed)

    return result

# Example usage
result1 = perform_division(10, 2)
result2 = perform_division(10, 0)

print("Result 1:", result1)
print("Result 2:", result2)

Output (Example):

This block always gets executed.
Result 1: 5.0
This block always gets executed.
Error: division by zero
Result 2: None

In this example, the finally block ensures that the specified code is executed regardless of whether an exception occurs or not. It is commonly used for cleanup tasks to maintain the integrity of the program's state.


8.
How do you create a custom exception in Python?

Creating a Custom Exception in Python:

In Python, you can create custom exceptions by defining a new class that inherits from the built-in Exception class or one of its subclasses. This allows you to raise and catch exceptions that are specific to your application or use case.

Here's an example program demonstrating the creation and use of a custom exception:

class CustomError(Exception):
    def __init__(self, message="Custom error occurred"):
        self.message = message
        super().__init__(self.message)

def custom_function(value):
    try:
        if value < 0:
            raise CustomError("Negative value not allowed")

        # Perform some operations with the value
        result = 10 / value

    except CustomError as ce:
        print(f"CustomError: {ce}")

    else:
        print("No custom error occurred.")

    finally:
        print("Cleanup or finalization code here.")

# Example usage
custom_function(5)
custom_function(-2)

Output (Example):

No custom error occurred.
CustomError: Negative value not allowed
Cleanup or finalization code here.

In this example, the CustomError class is created by inheriting from the base Exception class. When a negative value is passed to the custom_function, it raises the custom exception, and the corresponding except block handles it, printing the custom error message.