Exception handling

Exception handling is a key concept in Python used to manage errors and exceptions that occur during the execution of a program. Proper exception handling allows your program to respond gracefully to unexpected situations without crashing.

Why Exception Handling?

Errors in Python are categorized as exceptions. If an exception is not handled, it will terminate the program and produce an error message, which includes a traceback of the error. Using exception handling, you can manage these errors and prevent abrupt program crashes.

For example, dividing a number by zero raises a ZeroDivisionError. Without exception handling, the program would crash:

print(1 / 0)  # ZeroDivisionError: division by zero

With exception handling, we can handle this scenario more gracefully:

try:
    print(1 / 0)
except ZeroDivisionError:
    print("Error: Cannot divide by zero!")

The try-except Block

The basic syntax for handling exceptions in Python involves the try-except block:

try:
    # Code that may raise an exception
    result = 10 / 0
except ZeroDivisionError:
    # Code that handles the exception
    print("Cannot divide by zero.")

In this example:

  • The code inside the try block is executed.
  • If a ZeroDivisionError occurs, the code in the except block is executed instead of raising the exception.

Multiple Exceptions

You can handle multiple exceptions by specifying different except blocks for each type of exception:

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")
except ValueError:
    print("Invalid input.")

Alternatively, you can catch multiple exceptions in one except block using a tuple:

try:
    result = 10 / 0
except (ZeroDivisionError, ValueError):
    print("An error occurred.")

Catching All Exceptions

You can catch any exception by using a generic except block without specifying a type. However, this practice is generally discouraged as it can hide unexpected errors:

try:
    result = 10 / 0
except Exception as e:
    print(f"An error occurred: {e}")

In this case, e captures the exception instance, and you can print or log it for debugging.

The else Clause

The else clause can be used to specify code that should run if no exception is raised in the try block:

try:
    result = 10 / 2
except ZeroDivisionError:
    print("Cannot divide by zero.")
else:
    print(f"Result: {result}")  # Only executed if no exception occurs

The finally Clause

The finally block is always executed, whether an exception occurs or not. It’s typically used to clean up resources, like closing files or network connections:

try:
    file = open("data.txt", "r")
    data = file.read()
except FileNotFoundError:
    print("File not found.")
finally:
    file.close()  # This will always run, even if an exception occurs

The finally block ensures that cleanup code runs, regardless of whether an exception was raised.

Raising Exceptions

You can manually raise exceptions using the raise keyword. This is useful when you want to trigger an exception conditionally:

def check_positive(number: int) -> None:
    if number < 0:
        raise ValueError("Number must be positive!")
    print(f"Number is: {number}")

check_positive(-5)  # Raises a ValueError

You can also raise exceptions while preserving the original traceback, which helps in debugging:

try:
    raise ValueError("Initial error")
except ValueError as e:
    raise RuntimeError("Secondary error occurred") from e

Custom Exceptions

Python allows you to define custom exceptions by subclassing the built-in Exception class. This is useful when you want to handle domain-specific errors:

class InvalidAgeError(Exception):
    """Custom exception for invalid age."""
    pass

def set_age(age: int) -> None:
    if age < 0:
        raise InvalidAgeError("Age cannot be negative!")
    print(f"Age is: {age}")

try:
    set_age(-1)
except InvalidAgeError as e:
    print(e)  # Outputs: Age cannot be negative!

Creating custom exceptions provides more control over error handling in complex systems.

Common Mistakes

  • Catching all exceptions with except: This can lead to obscure bugs because it catches exceptions that you might not want to handle (e.g., KeyboardInterrupt, SystemExit).
# Problem
try:
    result = 1 / 0
except:
    print("Error occurred.")  # Catches all exceptions, including system ones.

# Solution
try:
    result = 1 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")
  • Raising exceptions without a message: Always provide informative messages when raising exceptions. It makes debugging easier.
# Problem
raise ValueError  # No message provided

# Solution
raise ValueError("A detailed error message")  # With message
  • Using finally for non-cleanup tasks: The finally block should only be used for cleanup tasks like closing files or freeing up resources. Avoid using it for logic that could affect the main flow of the program.

Track your progress

Mark this subtopic as completed when you finish reading.