intermediate Step 12 of 20

Error Handling

Python Programming

Error Handling

Errors are inevitable in programming. Users provide unexpected input, files go missing, network connections drop, and APIs return errors. Robust programs anticipate these problems and handle them gracefully instead of crashing. Python uses an exception handling mechanism based on try, except, else, and finally blocks that lets you catch and respond to errors in a structured way. Understanding exception handling is critical for building reliable, production-quality software.

Types of Errors

# SyntaxError — code cannot be parsed (caught at compile time)
# print("hello"   # SyntaxError: unexpected EOF

# Common runtime exceptions:
# print(10 / 0)           # ZeroDivisionError
# int("hello")            # ValueError
# my_list = [1,2,3]
# my_list[10]             # IndexError
# my_dict = {}
# my_dict["missing"]      # KeyError
# open("nonexistent.txt") # FileNotFoundError
# "hello" + 5             # TypeError
# import nonexistent      # ModuleNotFoundError

Try/Except Blocks

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

# Catching multiple exceptions
try:
    value = int(input("Enter a number: "))
    result = 100 / value
    print(f"Result: {result}")
except ValueError:
    print("That's not a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")

# Catching multiple exceptions in one block
try:
    data = [1, 2, 3]
    print(data[10])
except (IndexError, KeyError) as e:
    print(f"Lookup error: {e}")

# Accessing the exception object
try:
    int("not_a_number")
except ValueError as e:
    print(f"Error type: {type(e).__name__}")
    print(f"Error message: {e}")

# Catch-all (use sparingly)
try:
    risky_operation()
except Exception as e:
    print(f"Unexpected error: {e}")
    # Log the error, notify admin, etc.

Try/Except/Else/Finally

# Full exception handling structure
def read_config(filename):
    try:
        with open(filename, "r") as f:
            data = f.read()
    except FileNotFoundError:
        print(f"Config file '{filename}' not found, using defaults.")
        data = "{}"
    except PermissionError:
        print(f"No permission to read '{filename}'.")
        data = "{}"
    else:
        # Runs ONLY if no exception occurred
        print(f"Successfully read {len(data)} bytes from {filename}")
    finally:
        # ALWAYS runs, whether exception occurred or not
        print("Config loading complete.")
    return data

config = read_config("settings.json")

# Practical example: database-like transaction
def transfer_money(from_account, to_account, amount):
    try:
        from_account["balance"] -= amount
        if from_account["balance"] < 0:
            raise ValueError("Insufficient funds")
        to_account["balance"] += amount
    except ValueError as e:
        # Rollback
        from_account["balance"] += amount
        print(f"Transfer failed: {e}")
        return False
    else:
        print(f"Transferred ${amount:.2f} successfully")
        return True
    finally:
        print(f"From balance: ${from_account['balance']:.2f}")
        print(f"To balance: ${to_account['balance']:.2f}")

acct_a = {"balance": 100.0}
acct_b = {"balance": 50.0}
transfer_money(acct_a, acct_b, 75.0)

Raising Exceptions and Custom Exceptions

# Raising built-in exceptions
def set_age(age):
    if not isinstance(age, int):
        raise TypeError("Age must be an integer")
    if age < 0 or age > 150:
        raise ValueError(f"Age must be between 0 and 150, got {age}")
    return age

# Re-raising exceptions
try:
    result = some_function()
except ValueError:
    print("Logging error...")
    raise  # Re-raise the same exception

# Custom exception classes
class AppError(Exception):
    """Base exception for the application."""
    pass

class ValidationError(AppError):
    """Raised when input validation fails."""
    def __init__(self, field, message):
        self.field = field
        self.message = message
        super().__init__(f"Validation error on '{field}': {message}")

class NotFoundError(AppError):
    """Raised when a resource is not found."""
    pass

# Using custom exceptions
def create_user(name, email):
    if not name or len(name) < 2:
        raise ValidationError("name", "Name must be at least 2 characters")
    if "@" not in email:
        raise ValidationError("email", "Invalid email format")
    return {"name": name, "email": email}

try:
    user = create_user("", "invalid")
except ValidationError as e:
    print(f"Field: {e.field}")
    print(f"Error: {e.message}")
Pro tip: Follow the principle of catching specific exceptions rather than broad ones. Catching Exception can mask bugs by silently swallowing unexpected errors. Only catch the exceptions you know how to handle, and let others propagate up the call stack.

Key Takeaways

  • Use try/except to catch and handle exceptions; always catch specific exception types when possible.
  • The else block runs only when no exception occurs; finally always runs regardless of exceptions.
  • Use raise to throw exceptions and raise without arguments to re-raise the current exception.
  • Create custom exception classes by inheriting from Exception to provide domain-specific error types.
  • Never silently ignore exceptions — at minimum, log them so you can diagnose issues in production.