Handling Exceptions with try/except

Handling Exceptions with try/except

When you’re writing code in Python, things don’t always go as planned. You might try to open a file that doesn’t exist, divide a number by zero, or access a key in a dictionary that isn’t there. When these situations arise, Python raises an exception—a signal that an error or unusual condition has occurred. If you don’t handle these exceptions, your program will crash. That’s where try/except comes to the rescue.

In this article, we’ll explore how to use try/except blocks to gracefully handle errors, keep your programs running smoothly, and provide useful feedback to users. By the end, you’ll be confident in writing robust code that anticipates and manages unexpected issues.

The Basics of Try/Except

At its core, a try/except block allows you to "try" a piece of code that might cause an error and "except" (catch) that error if it occurs. This prevents the program from stopping abruptly and gives you a chance to respond appropriately.

Here’s a simple example:

try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"10 divided by your number is {result}")
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("You can't divide by zero!")

In this code, we try to convert user input to an integer and then divide 10 by that number. If the user enters something that isn’t a number, a ValueError is raised. If they enter zero, a ZeroDivisionError occurs. Each except clause handles a specific type of exception.

Catching Multiple Exceptions

You can catch multiple exceptions in a single except block by specifying them as a tuple. This is useful when you want to handle several errors in the same way.

try:
    # Some code that might raise exceptions
    value = my_list[index]
    result = 10 / value
except (IndexError, ZeroDivisionError) as e:
    print(f"An error occurred: {e}")

Here, both IndexError (if index is out of range) and ZeroDivisionError (if value is zero) are caught by the same except block. The as e part captures the exception object, which you can use to get more information about the error.

Exception Type Common Cause
ValueError Invalid value, e.g., converting a non-numeric string to int.
ZeroDivisionError Division or modulo operation with zero.
IndexError Accessing a list index that doesn’t exist.
KeyError Accessing a dictionary key that doesn’t exist.
FileNotFoundError Trying to open a file that doesn’t exist.

The Else and Finally Clauses

Sometimes you want to run code only if no exceptions were raised. That’s where the else clause comes in. Code in the else block runs only if the try block completes without any errors.

try:
    number = int(input("Enter a number: "))
except ValueError:
    print("Invalid input.")
else:
    print(f"You entered the number {number}.")

Additionally, you might have code that must run regardless of whether an exception occurred—like closing a file or releasing a resource. For that, use the finally clause.

file = None
try:
    file = open("data.txt", "r")
    content = file.read()
    print(content)
except FileNotFoundError:
    print("The file was not found.")
finally:
    if file:
        file.close()
    print("Cleanup completed.")

This ensures the file is closed even if an error happens while reading it.

Here are some common built-in exceptions you’re likely to encounter: - ValueError - TypeError - IndexError - KeyError - ZeroDivisionError - FileNotFoundError

Using Exception Hierarchies

Python exceptions are organized in a hierarchy. For example, ArithmeticError is a parent class of ZeroDivisionError. You can catch a parent exception to handle all its subclasses.

try:
    # Risky operation
    risky_call()
except ArithmeticError:
    print("An arithmetic error occurred.")

This will catch any exception that is a subclass of ArithmeticError, including ZeroDivisionError and OverflowError. Be cautious: catching too broad an exception (like Exception) can mask errors you didn’t anticipate.

Custom Exceptions

You can define your own exceptions by creating a new class that inherits from Python’s Exception class. This is useful for application-specific errors.

class NegativeNumberError(Exception):
    pass

def check_positive(number):
    if number < 0:
        raise NegativeNumberError("Number must be positive.")
    return number

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

Here, we define a custom exception NegativeNumberError and raise it when a negative number is passed to check_positive.

Custom Exception Purpose
InvalidInputError Raised when user input doesn’t meet criteria.
ConnectionTimeoutError Raised when a network connection times out.
InsufficientFundsError Raised in banking apps when withdrawal exceeds balance.

Best Practices for Exception Handling

While try/except is powerful, it’s important to use it wisely. Avoid using a bare except clause (one without specifying an exception type), as it can catch unexpected errors like KeyboardInterrupt (Ctrl+C) and make debugging difficult.

Instead, always try to catch specific exceptions. If you need to catch all exceptions for logging or cleanup, use except Exception:, but remember to re-raise or handle appropriately.

Also, don’t use exceptions for control flow. For example, don’t use try/except to check if a list index exists—use an if statement with len(my_list) instead.

Here’s an example of what to avoid:

# Not recommended
try:
    value = my_list[index]
except IndexError:
    value = None

# Better
if index < len(my_list):
    value = my_list[index]
else:
    value = None

The second approach is clearer and more efficient.

Real-World Example: Reading a Config File

Let’s look at a practical example: reading a configuration file. We’ll handle possible file errors and data parsing issues.

import json

config = {}
try:
    with open("config.json", "r") as file:
        config = json.load(file)
except FileNotFoundError:
    print("Config file not found. Using defaults.")
except json.JSONDecodeError:
    print("Invalid JSON in config file.")
else:
    print("Config loaded successfully.")
finally:
    print("Configuration process complete.")

This code tries to read and parse a JSON config file. If the file isn’t found, it falls back to defaults. If the JSON is malformed, it alerts the user. The else block runs on success, and the finally block always executes.

List of common steps when handling file operations: - Open the file inside a try block. - Catch FileNotFoundError if the file doesn’t exist. - Catch parsing errors (e.g., JSONDecodeError for JSON files). - Use finally to ensure resources are released.

Logging Exceptions

In larger applications, you might want to log exceptions for debugging instead of just printing them. The logging module is great for this.

import logging

logging.basicConfig(level=logging.ERROR)

try:
    # Code that may fail
    risky_operation()
except Exception as e:
    logging.error("An error occurred", exc_info=True)

This logs the full error traceback, which is helpful for diagnosing issues in production.

Conclusion

Exception handling with try/except is a fundamental skill for writing resilient Python code. By anticipating errors and handling them gracefully, you improve the user experience and make your programs more reliable. Remember to catch specific exceptions, use else and finally where appropriate, and avoid overusing try/except for control flow. With practice, you’ll be able to tackle errors confidently and keep your applications running smoothly.