Python Exception Handling

Exception handling in Python allows us to manage runtime errors, ensuring our programs run smoothly even when unexpected issues arise. Python's built-in exception handling mechanism is based on `try`, `except`, `else`, `finally`, and `raise` statements.

1. Basic Exception Handling

The `try` block is used to wrap code that might throw an exception. If an exception occurs, the `except` block handles it, allowing the program to continue running.
# Basic try-except example
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")

Output:
Error: Division by zero is not allowed.

Explanation: Attempting to divide by zero raises a `ZeroDivisionError`, which is caught and handled in the `except` block.

2. Handling Multiple Exceptions

We can handle multiple exceptions by specifying different types in separate `except` blocks. This allows different handling for different types of errors.
# Handling multiple exceptions
try:
    num = int("ABC")
    result = 10 / num
except ValueError:
    print("Error: Invalid input. Cannot convert to integer.")
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")

Output:
Error: Invalid input. Cannot convert to integer.

Explanation: The attempt to convert `"ABC"` to an integer raises a `ValueError`, which is handled by the appropriate `except` block.

3. Catching Multiple Exceptions in a Single Block

In cases where the handling for different exceptions is the same, we can catch multiple exceptions in a single `except` block by grouping them in parentheses.
try:
    num = int("ABC")
    result = 10 / num
except (ValueError, ZeroDivisionError):
    print("Error: An invalid operation occurred.")

Output:
Error: An invalid operation occurred.

Explanation: Here, both `ValueError` and `ZeroDivisionError` are handled in the same way, so they’re grouped in a single `except` block.

4. Using `else` with `try` and `except`

The `else` block runs only if the `try` block succeeds without any exceptions. It’s useful for code that should execute only when no errors occur.
try:
    num = int("10")
    result = 10 / num
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")
else:
    print("Success! Result is:", result)

Output:
Success! Result is: 1.0

Explanation: Since no exception occurs, the `else` block executes and prints the result.

5. Using `finally`

The `finally` block always executes, regardless of whether an exception was raised or not. It's typically used for cleanup actions.
try:
    num = int("10")
    result = 10 / num
except ZeroDivisionError:
    print("Error: Division by zero is not allowed.")
else:
    print("Success! Result is:", result)
finally:
    print("This runs no matter what.")

Output:
Success! Result is: 1.0
This runs no matter what.

Explanation: The `finally` block runs after the `try` and `else` blocks, whether an exception occurred or not.

6. Raising Exceptions with `raise`

The `raise` keyword allows us to throw an exception manually, which can be used for custom error handling or enforcing constraints.
# Raising an exception
try:
    age = -1
    if age < 0:
        raise ValueError("Age cannot be negative.")
except ValueError as e:
    print("Caught exception:", e)

Output:
Caught exception: Age cannot be negative.

Explanation: A `ValueError` is manually raised with a custom message if `age` is negative, which is then caught and printed in the `except` block.

7. Custom Exception Classes

Python allows us to define custom exceptions by creating new classes that inherit from the `Exception` class. This is useful for more specific error handling.
# Define a custom exception
class NegativeAgeError(Exception):
    pass

# Using custom exception
try:
    age = -5
    if age < 0:
        raise NegativeAgeError("Negative age is not allowed.")
except NegativeAgeError as e:
    print("Caught custom exception:", e)

Output:
Caught custom exception: Negative age is not allowed.

Explanation: `NegativeAgeError` is a custom exception class that inherits from `Exception`, and it’s used to raise an error if `age` is negative.

8. Getting Exception Details

We can capture exception details using the `as` keyword, which stores the exception instance in a variable, providing access to additional details.
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print("Exception:", e)

Output:
Exception: division by zero

Explanation: The `ZeroDivisionError` instance `e` holds additional information about the exception, which is printed in the output.

9. Ignoring Exceptions

We can ignore an exception by using an empty `except` block. While this is generally not recommended, it can be useful in specific scenarios.
try:
    result = 10 / 0
except ZeroDivisionError:
    pass  # Ignore the exception
print("This line still executes.")

Output:
This line still executes.

Explanation: The `except` block catches and ignores the `ZeroDivisionError`, allowing the program to continue without interruption.

10. Using `try` with `finally` Alone

The `try` block can be paired with only a `finally` block, which executes regardless of whether an exception occurs. This is typically used for cleanup actions that must run no matter what.
try:
    print("Executing some code.")
finally:
    print("This always executes, even if an error occurs.")

Output:
Executing some code.
This always executes, even if an error occurs.

Explanation: Even without an `except` block, `finally` ensures the cleanup code runs.

Summary

Python’s exception handling mechanism with `try`, `except`, `else`, `finally`, and `raise` provides flexibility and control over error management, ensuring robust and resilient code. Custom exceptions allow for tailored error handling, enhancing the readability and maintainability of your Python programs.

Previous: String Operations | Next: Python Functions

<
>