try/except

Difficulty: Intermediate

Error handling in Python revolves around the try/except statement. When you place code inside a try block, Python attempts to execute it normally. If an exception occurs during execution, Python immediately stops running the try block and looks for a matching except clause. If it finds one, the code in that except block runs, and the program continues normally after the entire try/except structure.

Catching specific exceptions is a best practice in Python. Instead of catching all exceptions with a bare except or except Exception, you should catch the most specific exception type you expect. Python has a rich hierarchy of built-in exceptions: ValueError for invalid values, TypeError for wrong types, KeyError for missing dictionary keys, IndexError for out-of-range indices, ZeroDivisionError for division by zero, and FileNotFoundError for missing files, among many others. Catching specific exceptions makes your code more predictable and easier to debug.

Multiple except blocks allow you to handle different exception types with different logic. Python checks each except clause in order, from top to bottom, and executes the first one that matches the raised exception. Because of this top-down matching, you should always place more specific exception types before more general ones. If you put Exception first, it would catch everything and the more specific handlers below it would never run.

You can also catch multiple exception types in a single except clause by grouping them in a tuple, such as except (ValueError, TypeError) as e. This is useful when you want to handle several exception types the same way. The as keyword binds the exception object to a variable, giving you access to the error message and other attributes for logging or user feedback.

Understanding Python's exception hierarchy is important for effective error handling. All exceptions inherit from BaseException, but most exceptions you will catch inherit from Exception. SystemExit, KeyboardInterrupt, and GeneratorExit inherit directly from BaseException, which is why catching Exception does not intercept Ctrl+C or sys.exit() calls. This design ensures that a broad except Exception clause does not accidentally prevent a program from shutting down gracefully.

Code examples

Basic try/except with specific exception

def safe_divide(a, b):
    try:
        result = a / b
        print(f"{a} / {b} = {result}")
    except ZeroDivisionError as e:
        print(f"Caught: {e}")

safe_divide(10, 3)
safe_divide(10, 0)

The first call succeeds and prints the result. The second call raises ZeroDivisionError, which is caught by the except clause. The 'as e' syntax captures the exception object so we can print its message.

Multiple except blocks for different exceptions

def process_item(data, index):
    try:
        value = data[index]
        result = int(value)
        print(f"Processed: {result}")
    except IndexError:
        print("Error: Index out of range")
    except ValueError:
        print("Error: Cannot convert to integer")
    except TypeError:
        print("Error: Invalid data type")

items = ["42", "hello", "7"]
process_item(items, 0)
process_item(items, 1)
process_item(items, 5)
process_item(None, 0)

Each call triggers a different exception. Index 0 succeeds, index 1 has a non-numeric string causing ValueError, index 5 is out of range causing IndexError, and None is not subscriptable causing TypeError. Each exception is handled by its own except block.

Catching multiple exceptions in one clause

def get_value(data, key):
    try:
        return data[key]
    except (KeyError, TypeError) as e:
        print(f"Access error ({type(e).__name__}): {e}")
        return None

result1 = get_value({"a": 1}, "b")
print(f"Result 1: {result1}")
result2 = get_value(None, "a")
print(f"Result 2: {result2}")

Both KeyError and TypeError are caught by the same except clause using a tuple. The type(e).__name__ expression prints the actual exception class name so we can tell which error occurred.

Exception ordering matters

def check_age(age):
    try:
        age = int(age)
        if age < 0:
            raise ValueError("Age cannot be negative")
        if age > 150:
            raise ValueError("Age seems unrealistic")
        print(f"Valid age: {age}")
    except ValueError as e:
        print(f"ValueError: {e}")
    except Exception as e:
        print(f"Unexpected error: {e}")

check_age("25")
check_age("-5")
check_age("abc")
check_age("200")

ValueError is listed before Exception because ValueError is a subclass of Exception. If Exception came first, it would catch everything and the specific ValueError handler would never execute. Always order except blocks from most specific to most general.

Key points

Concepts covered

try/except, specific exceptions, multiple except blocks, exception hierarchy, bare except