Lightning bolt and Python code snippet with "Python Error Handling" in blocky caps

Python Error Handling

In this lesson, we’ll cover error handling in Python, focusing on how to manage exceptions, raise custom exceptions, and clean up resources using try, except, finally, and more.

Proper error handling ensures that your program recovers gracefully from unexpected conditions without crashing.

What is an Exception?

An exception is an error that occurs during the execution of a program. When an exception occurs, Python halts the normal flow of the program and raises an exception. If the exception is not handled, the program will terminate with a traceback message showing where the error occurred.

Common Exceptions in Python:

Some of the most common built-in exceptions in Python include:

  • ZeroDivisionError: Raised when dividing by zero.
  • TypeError: Raised when an operation is applied to an object of an inappropriate type.
  • ValueError: Raised when a function receives an argument of the correct type but an invalid value.
  • IndexError: Raised when trying to access an index that is out of range.
  • KeyError: Raised when trying to access a key that does not exist in a dictionary.
  • FileNotFoundError: Raised when trying to open a file that does not exist.

Example of an Unhandled Exception:

# Division by zero error
result = 10 / 0

This will raise a ZeroDivisionError and cause the program to terminate with a traceback:

ZeroDivisionError: division by zero

Handling Exceptions with try and except

To handle exceptions, you can use a try block to catch and manage exceptions with except. This prevents your program from crashing and allows you to respond gracefully to errors.

Basic Syntax:

try:
    # Code that might raise an exception
    risky_operation()
except SomeException:
    # Code to handle the exception
    print("An error occurred.")

Example:

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

Output:

Cannot divide by zero.

In this example, Python catches the ZeroDivisionError and executes the except block instead of terminating the program.

Catching Multiple Exceptions

You can catch multiple types of exceptions by specifying them in separate except blocks, or you can catch multiple exceptions in one block using a tuple.

Example:

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

In this case, if an exception is raised, the appropriate except block will be executed.

Alternatively, you can catch both exceptions in one block:

try:
    value = int("abc")
except (ValueError, ZeroDivisionError):
    print("An error occurred.")

Handling All Exceptions

To catch any type of exception, you can use a bare except block, but this should be used with caution because it will catch all exceptions, including those you might not expect.

Example:

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

This will catch all exceptions, but it’s better to catch specific exceptions to ensure you only handle the errors you expect. Catching all exceptions can make debugging difficult, as it hides important information.

Using else and finally

  • else: The else block runs if no exception was raised in the try block.
  • finally: The finally block is always executed, regardless of whether an exception was raised or not. It’s useful for cleaning up resources, like closing files or network connections.

Example with else and finally:

try:
    result = 10 / 2
except ZeroDivisionError:
    print("Cannot divide by zero.")
else:
    print(f"Result: {result}")
finally:
    print("This will always execute.")

Output:

Result: 5.0
This will always execute.

In this example:

  • The else block runs if no exceptions occur.
  • The finally block always runs, making it useful for cleanup tasks like closing files or database connections.

Raising Exceptions

You can raise exceptions manually using the raise statement. This is useful when you want to signal an error or enforce certain conditions in your code.

Example:

def divide(a, b):
    if b == 0:
        raise ValueError("b cannot be zero.")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(e)

Output:

b cannot be zero.

In this example, the function divide() raises a ValueError if b is zero, and the exception is handled in the try block.

Creating Custom Exceptions

You can define your own exceptions by creating a class that inherits from Python’s built-in Exception class. This allows you to create custom error types for your specific use cases.

Example:

class InvalidAgeError(Exception):
    pass

def set_age(age):
    if age < 0:
        raise InvalidAgeError("Age cannot be negative.")
    print(f"Age is set to {age}")

try:
    set_age(-1)
except InvalidAgeError as e:
    print(e)

Output:

Age cannot be negative.

In this example, InvalidAgeError is a custom exception, and it’s raised when the age is invalid. Custom exceptions are useful for making your error messages more specific and meaningful.

Cleaning Up Resources with finally

The finally block is often used to clean up resources like closing files, database connections, or network sockets, regardless of whether an exception occurred or not.

Example:

try:
    file = open("example.txt", "r")
    # Perform file operations
except FileNotFoundError:
    print("File not found.")
finally:
    file.close()  # This will always execute

In this example, the finally block ensures that the file is closed, even if an exception occurs.

Using with for Automatic Resource Management

Python provides the with statement, which simplifies resource management. When using with, Python automatically handles opening and closing resources, even if an exception occurs. This is especially useful for managing files, sockets, and database connections.

Example:

with open("example.txt", "r") as file:
    content = file.read()
    print(content)

The with statement automatically closes the file when the block is exited, so you don’t need to call file.close() manually. This is known as a context manager and is the preferred way to manage resources in Python.

Example: Building a Robust Program

Let’s build a simple program that reads numbers from a file and divides them by a given number. We’ll handle potential errors like file not found, invalid data, and division by zero.

Example Program:

def divide_numbers(filename, divisor):
    try:
        with open(filename, "r") as file:
            numbers = [int(line.strip()) for line in file]
        result = [num / divisor for num in numbers]
        return result
    except FileNotFoundError:
        print("Error: The file was not found.")
    except ValueError:
        print("Error: The file contains non-numeric data.")
    except ZeroDivisionError:
        print("Error: Cannot divide by zero.")
    finally:
        print("Execution finished.")

# Test the program
result = divide_numbers("numbers.txt", 0)

In this program:

  • We use with to safely open and close the file.
  • We handle three possible exceptions: FileNotFoundError, ValueError, and ZeroDivisionError.
  • The finally block ensures that a final message is printed, regardless of what happens during execution.

Key Concepts Recap

  • Exceptions are runtime errors that can be handled using try and except.
  • Use else to specify code that runs if no exception occurs, and finally for cleanup tasks.
  • You can raise exceptions manually using the raise statement.
  • Custom exceptions can be defined by subclassing the Exception class.
  • The with statement is a useful tool for managing resources like files, ensuring they are properly closed even if an error occurs.

Exercises:

  1. Write a program that reads an integer from the user and raises a ValueError if the input is not a valid integer. Use try, except, and finally to handle the error and display a message at the end of the program.
  2. Create a custom exception class called NegativeNumberError that is raised when a function is passed a negative number.
  3. Write a function that reads a list of numbers from a file and divides each by a user-provided value. Handle errors like file not found, division by zero, and invalid data.
  4. Implement a context manager using the with statement to manage a resource (like opening and closing a file).

Next time, we’ll explore File I/O in Python, including reading from and writing to files, working with different file formats, and managing directories.

FAQ

Q1: Why should I use try and except instead of letting the program crash?

A1:
Using try and except allows you to handle errors gracefully rather than letting the program crash abruptly. By catching exceptions, you can display meaningful error messages, log the issue, attempt to recover from the error, or provide alternative solutions. This makes your program more user-friendly and robust.

For example, if your program tries to divide by zero, handling the error prevents a crash and allows you to take corrective action:

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

Q2: Is it bad practice to catch all exceptions with a bare except?

A2:
Yes, catching all exceptions can be bad practice because it hides potential errors you might not be aware of. It can make debugging difficult since you won’t know what went wrong. It’s better to catch specific exceptions to handle only the errors you expect.

If you need to catch all exceptions temporarily for testing or logging purposes, you can include the exception object to gain more information:

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

This way, you at least know what type of error was raised.

Q3: What’s the difference between else and finally in error handling?

A3:

  • else: The else block is executed only if no exceptions are raised in the try block. It’s useful for placing code that should only run when the try block is successful. Example:
  try:
      result = 10 / 2
  except ZeroDivisionError:
      print("Cannot divide by zero.")
  else:
      print(f"Result: {result}")  # Only runs if no exception occurs
  • finally: The finally block is always executed, regardless of whether an exception occurred or not. It’s typically used for cleaning up resources, such as closing files or network connections. Example:
  try:
      result = 10 / 0
  except ZeroDivisionError:
      print("Cannot divide by zero.")
  finally:
      print("Cleanup: This will always execute.")

In summary, use else for code that should only run if no exceptions occur, and finally for cleanup tasks that need to run regardless of the outcome.

Q4: Can I raise multiple exceptions in the same block?

A4:
Yes, you can raise multiple exceptions in different parts of a block, but you’ll need to handle them in separate except blocks. Python doesn’t allow raising multiple exceptions at once, but you can chain exceptions or handle them sequentially.

Example:

def check_values(a, b):
    if a < 0:
        raise ValueError("a cannot be negative.")
    if b == 0:
        raise ZeroDivisionError("b cannot be zero.")

try:
    check_values(-1, 0)
except ValueError as ve:
    print(ve)
except ZeroDivisionError as zde:
    print(zde)

Alternatively, you can raise a different exception to indicate multiple issues using exception chaining:

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

Q5: How does exception handling work with loops?

A5:
When this happens, the loop is interrupted, and the program jumps to the nearest except block. If the exception is handled, the loop can continue running (if desired). You can wrap loop iterations in a try block to handle exceptions on a per-iteration basis.

Example:

numbers = [10, 5, 0, 2]

for number in numbers:
    try:
        result = 10 / number
        print(f"Result: {result}")
    except ZeroDivisionError:
        print("Cannot divide by zero.")

Output:

Result: 1.0
Result: 2.0
Cannot divide by zero.
Result: 5.0

Here, the loop continues even after encountering a ZeroDivisionError, allowing it to process the remaining elements.

Q6: When should I use custom exceptions?

A6:
Use custom exceptions when you need to signal a specific error condition that is not covered by Python’s built-in exceptions. Custom exceptions can make your code more readable and help differentiate between different types of errors in complex systems.

Example:

class NegativeNumberError(Exception):
    pass

def check_number(number):
    if number < 0:
        raise NegativeNumberError("Number cannot be negative.")

try:
    check_number(-5)
except NegativeNumberError as e:
    print(e)

Custom exceptions are especially useful in larger programs where you need to distinguish between multiple types of errors or communicate domain-specific issues.

Q7: Can I re-raise an exception after handling it?

A7:
Yes, you can use a plain raise statement without specifying the exception. This allows the exception to propagate to the calling code after you’ve performed some handling.

Example:

try:
    value = int("abc")
except ValueError:
    print("Caught a ValueError.")
    raise  # Re-raise the exception

Output:

Caught a ValueError.
ValueError: invalid literal for int() with base 10: 'abc'

In this example, the ValueError is caught, and a message is printed, but the exception is re-raised so it can be handled by higher-level code if needed.

Q8: What’s the purpose of using the with statement for file handling?

A8:
The with statement simplifies file handling by ensuring that the file is properly closed after its block of code is executed, even if an exception occurs.

This eliminates the need for manually calling file.close(), which can be error-prone if not handled correctly in the presence of exceptions.

Example:

with open("example.txt", "r") as file:
    content = file.read()

Here, the file is automatically closed when the block is exited, whether an exception occurs or not. Without with, you would need to handle closing the file manually, especially in the case of errors:

try:
    file = open("example.txt", "r")
    content = file.read()
finally:
    file.close()  # Manually closing the file

The with statement is more concise and safer for managing resources like files, sockets, or database connections.

Q9: What happens if an exception is raised inside a finally block?

A9:
It will override any previous exceptions raised in the try or except blocks. This means the original exception is suppressed, and only the exception from the finally block will be propagated.

Example:

try:
    raise ValueError("An error occurred.")
finally:
    raise RuntimeError("Error in finally block.")

Output:

RuntimeError: Error in finally block.

In this case, the RuntimeError raised in the finally block overrides the ValueError, so only the RuntimeError is raised. Be careful when raising exceptions in a finally block, as it can hide earlier exceptions.

Q10: Can I create a context manager for non-file resources?

A10:
Yes, by using the with statement and defining a class with the __enter__() and __exit__() methods.

This allows you to manage any type of resource (e.g., database connections, locks) that needs proper initialization and cleanup.

Example:

class MyResource:
    def __enter__(self):
        print("Resource acquired")
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        print("Resource released")

# Using the context manager
with MyResource():
    print("Using the resource")

Output:

Resource acquired
Using the resource
Resource released

This example demonstrates how to create a custom context manager that acquires and releases a resource. The __enter__() method runs when the block starts, and the __exit__() method runs when the block is exited, ensuring proper resource management.

Similar Posts