Title: Understanding Python Exceptions

To kick things off: what exactly is an exception? Well, in programming terms, an exception is something that goes wrong during the normal flow of your code. This could be anything from trying to divide by zero (which will always result in an error) to attempting to open a file that doesn’t exist on disk.
In Python, exceptions are raised when one of these errors occurs. A “raise” statement is used to explicitly raise an exception if you want to throw an error yourself. For example:

# This function takes in two numbers and attempts to divide them
def divide_numbers(x, y):
    try: # try block is used to handle potential errors
        result = x / y # performs division operation
    except ZeroDivisionError: # handles the specific error of dividing by zero
        print("You can't divide by zero!") # prints an error message
    return result # returns the result of the division operation

# Example usage of the function
print(divide_numbers(10, 2)) # prints 5
print(divide_numbers(10, 0)) # prints "You can't divide by zero!" and returns None

In this code snippet, we define a function called `divide_numbers()`. If the division operation (x/y) results in a `ZeroDivisionError`, then Python will catch that error and print out an appropriate message. The rest of the code won’t be executed if an exception is raised.
But what happens when you don’t explicitly raise an exception? Let’s say we have this code:

# This function takes in a filename as a parameter and attempts to open and read the file.
def open_file(filename):
    try:
        # The 'with' statement automatically closes the file after the code block is executed.
        # The 'r' mode indicates that the file will be opened for reading.
        with open(filename, 'r') as f:
            # The contents of the file are read and stored in the 'contents' variable.
            contents = f.read()
    # If the file is not found, a FileNotFoundError exception is raised.
    except FileNotFoundError:
        # A message is printed to inform the user that the file was not found.
        print("File not found!")
        
    # The contents of the file are returned.
    return contents

In this example, we’re trying to read a file using the `open()` function in Python. If that file doesn’t exist on disk (i.e., it throws a `FileNotFoundError`), then our code will catch that error and print out an appropriate message. Again, if an exception is raised, then the rest of the code won’t be executed.
But what about when you want to execute some code regardless of whether or not an exception was thrown? That’s where the `finally` keyword comes in handy:

# This function takes in a filename and attempts to open and read the file
def close_file(filename):
    try:
        # The 'with' statement automatically closes the file after the code block is executed
        with open(filename, 'r') as f:
            # The contents of the file are read and stored in the 'contents' variable
            contents = f.read()
    # If the file is not found, a FileNotFoundError exception is raised
    except FileNotFoundError:
        # A message is printed to inform the user that the file was not found
        print("File not found!")
    # The 'finally' keyword ensures that this code block is always executed, regardless of whether or not an exception was raised
    finally:
        # A message is printed to inform the user that the file is being closed
        print("Closing file...")
        
    # The contents of the file are returned
    return contents

In this example, we’re using the `finally` keyword to execute some code after our try/except block. Regardless of whether or not a `FileNotFoundError` is raised (or any other exception for that matter), Python will always print out “Closing file…” before returning from the function.
Now creating custom exceptions in Python! This can be helpful if you want to create more specific error messages for your code. For example:

# Creating a custom exception class in Python
# This class will allow us to create more specific error messages for our code.

class PlatformError(Exception): # Defines a new class called PlatformError that inherits from the Exception class
    def __init__(self, message): # Defines a constructor method that takes in a message parameter
        self.message = message # Assigns the message parameter to the instance variable "message"
        
    def __str__(self): # Defines a string representation method for the class
        return f"{self.__class__.__name__}: {self.message}" # Returns a string with the class name and the message
        
    def __repr__(self): # Defines a representation method for the class
        return str(self) # Returns a string representation of the class

In this code snippet, we’ve created a custom exception called `PlatformError`. This error can be raised if something goes wrong on a specific platform (e.g., macOS or Windows). When an instance of this class is created, it takes in a message that will be displayed when the exception is thrown.
Now let’s see how we can use our custom exception:

# Custom exception class for platform errors
class PlatformError(Exception):
    # Constructor that takes in a message to be displayed when the exception is thrown
    def __init__(self, message):
        self.message = message
    
    # Method to return the message when the exception is raised
    def __str__(self):
        return self.message

# Function to perform Linux-specific tasks
def linux_interaction():
    try:
        # Do some Linux-specific stuff here...
        print("Performing Linux-specific tasks...")
        
    # Catching the custom exception and printing the message
    except PlatformError as e:
        print(e)
    
    # Finally block to always execute code, regardless of whether an exception was thrown or not
    finally:
        print("Closing file...")

# Example of using the custom exception
try:
    # Simulating a platform error by raising the custom exception
    raise PlatformError("Something went wrong on the Linux platform.")
except PlatformError as e:
    print(e)

# Calling the function to perform Linux-specific tasks
linux_interaction()

# Output:
# Something went wrong on the Linux platform.
# Performing Linux-specific tasks...
# Closing file...

In this example, we’re using our custom `PlatformError` exception to handle any errors that might occur on a Linux-specific platform. If an error is raised (i.e., the try block throws an exception), then Python will catch it and display the appropriate message. The rest of the code won’t be executed if an exception is thrown, but our `finally` block will always execute regardless of whether or not an exception was thrown.

SICORPS