Python - Exception Handling

13.
Discuss the use of the with statement in the context of exception handling.

The with statement in Python is used to simplify the management of resources, such as file handling, by ensuring that certain operations are performed before and after the block of code inside the with statement. This is especially useful for exception handling because it guarantees that cleanup actions will be taken even if an exception is raised.

try:
    # Open a file using with statement
    with open("example.txt", "r") as file:
        content = file.read()
        print(content)

except FileNotFoundError as e:
    print(f"Error: {e}. The specified file was not found.")

except PermissionError as e:
    print(f"Error: {e}. Permission denied to access the file.")

except IOError as e:
    print(f"Error: {e}. An I/O error occurred while reading the file.")

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

else:
    print("File read successfully.")

finally:
    print("File handling completed.")

In this example, the with statement is used to open the file "example.txt" for reading. The file is automatically closed when the block inside the with statement is exited, even if an exception occurs. This ensures proper resource cleanup without the need for an explicit file.close() statement.

The output will be:

Error: [Errno 2] No such file or directory: 'example.txt'. The specified file was not found.
File handling completed.

14.
How can you re-raise an exception in Python?

In Python, you can re-raise an exception using the raise statement without providing any additional arguments. This allows you to capture an exception, perform some actions, and then raise the same exception to propagate it further. This can be useful in scenarios where you want to log information or perform custom handling before letting the exception continue its normal flow.

def example_function():
    try:
        # Some code that may raise an exception
        x = 1 / 0

    except ZeroDivisionError as e:
        # Handle the exception
        print(f"Caught an exception: {e}")

        # Perform some actions

        # Re-raise the exception
        raise

try:
    example_function()

except ZeroDivisionError as e:
    print(f"Exception propagated: {e}")

In this example, the example_function attempts to perform a division by zero, which raises a ZeroDivisionError. The exception is caught within the function, some actions are performed, and then the same exception is re-raised using the raise statement. Finally, the exception is caught again in the outer scope.

The output will be:

Caught an exception: division by zero
Exception propagated: division by zero

15.
What is the purpose of the sys.exc_info() function in exception handling?

The sys.exc_info() function in Python is used to get information about the most recent exception that occurred. It returns a tuple containing information about the current exception, including the exception type, the exception instance, and the traceback.

import sys

def example_function():
    try:
        # Some code that may raise an exception
        x = 1 / 0

    except ZeroDivisionError as e:
        # Get information about the exception
        exc_type, exc_value, exc_traceback = sys.exc_info()

        # Print exception details
        print(f"Exception Type: {exc_type}")
        print(f"Exception Value: {exc_value}")
        print(f"Exception Traceback: {exc_traceback}")

try:
    example_function()

except ZeroDivisionError as e:
    print(f"Caught an exception: {e}")

In this example, the example_function attempts to perform a division by zero, which raises a ZeroDivisionError. Inside the except block, sys.exc_info() is used to retrieve information about the exception. The exception type, value, and traceback are then printed.

The output will be:

Exception Type: <class 'ZeroDivisionError'>
Exception Value: division by zero
Exception Traceback: <traceback object at 0x...>
Caught an exception: division by zero

16.
Explain the concept of the try-except-else-finally hierarchy.

The try-except-else-finally hierarchy in Python is used for exception handling. It provides a structured way to handle exceptions and execute cleanup code regardless of whether an exception occurs or not.

Here's an example program that demonstrates the use of the try-except-else-finally hierarchy:

def example_function(divisor):
    try:
        result = 10 / divisor

    except ZeroDivisionError:
        print("Cannot divide by zero!")

    else:
        print("Division successful. Result:", result)

    finally:
        print("This will always be executed, regardless of exceptions.")

# Example usage
example_function(2)
example_function(0)

In this example, the example_function attempts to perform a division, and the try block contains the code that might raise an exception. The except block handles the specific exception (ZeroDivisionError) that may occur. The else block contains code that should run if no exception occurs. The finally block contains code that will always run, regardless of whether an exception occurred or not.

The output will be:

Division successful. Result: 5.0
Cannot divide by zero!
This will always be executed, regardless of exceptions.
This will always be executed, regardless of exceptions.