What Are Exceptions?

Exceptions are errors that occur during program execution. Without handling, they crash your program with a traceback message. Error handling lets you anticipate problems and respond gracefully.

# This crashes the program
number = int("hello")    # ValueError: invalid literal for int()

# This file might not exist
file = open("missing.txt")   # FileNotFoundError

Try/Except Basics

Wrap risky code in a try block and handle errors in except:

try:
    number = int(input("Enter a number: "))
    print(f"You entered: {number}")
except ValueError:
    print("That's not a valid number!")

If the code in try raises a ValueError, Python jumps to the except block instead of crashing.

Common Exception Types

# ValueError — wrong value type
int("hello")

# TypeError — wrong operation for type
"hello" + 5

# ZeroDivisionError — division by zero
10 / 0

# FileNotFoundError — file doesn't exist
open("nonexistent.txt")

# KeyError — dictionary key not found
d = {"a": 1}
d["b"]

# IndexError — list index out of range
lst = [1, 2, 3]
lst[10]

# NameError — variable not defined
print(undefined_var)

# AttributeError — object has no such attribute
"hello".nonexistent_method()

Catching Multiple Exceptions

# Separate handlers for different errors
try:
    value = int(input("Enter a number: "))
    result = 100 / value
    print(f"Result: {result}")
except ValueError:
    print("Please enter a valid number.")
except ZeroDivisionError:
    print("Cannot divide by zero!")

# Catch multiple types in one handler
try:
    data = process_input()
except (ValueError, TypeError, KeyError) as e:
    print(f"Input error: {e}")

The Full Try/Except Structure

try:
    file = open("data.txt", "r")
    content = file.read()
    number = int(content)
except FileNotFoundError:
    print("File not found!")
    number = 0
except ValueError:
    print("File doesn't contain a valid number!")
    number = 0
else:
    # Runs ONLY if no exception occurred
    print(f"Successfully read number: {number}")
finally:
    # ALWAYS runs, whether or not an exception occurred
    print("Operation complete.")
💡
When to use else vs finally

else runs only on success — use it for code that should only execute if no errors occurred. finally always runs — use it for cleanup tasks like closing connections or releasing resources.

Accessing Exception Details

try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error type: {type(e).__name__}")   # ZeroDivisionError
    print(f"Error message: {e}")                # division by zero

Raising Exceptions

Use raise to throw your own exceptions:

def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    if age > 150:
        raise ValueError("Age seems unrealistic")
    return age

try:
    user_age = set_age(-5)
except ValueError as e:
    print(f"Invalid age: {e}")    # Invalid age: Age cannot be negative

Custom Exception Classes

class InsufficientFundsError(Exception):
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(
            f"Cannot withdraw ${amount}. Balance: ${balance}"
        )

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(balance, amount)
    return balance - amount

try:
    new_balance = withdraw(100, 150)
except InsufficientFundsError as e:
    print(e)           # Cannot withdraw $150. Balance: $100
    print(e.balance)   # 100
    print(e.amount)    # 150

Best Practices

⚠️
Never use bare except:

Catching all exceptions hides bugs and makes debugging impossible. Always catch specific exception types.

# BAD — catches everything, hides bugs
try:
    do_something()
except:
    pass

# BAD — too broad
try:
    do_something()
except Exception:
    pass

# GOOD — catch specific errors
try:
    do_something()
except (ValueError, KeyError) as e:
    print(f"Known error: {e}")

# GOOD — catch broad only when logging
try:
    do_something()
except Exception as e:
    print(f"Unexpected error: {e}")
    raise    # Re-raise so the error isn't silently swallowed

Practical Examples

Safe User Input

def get_integer(prompt, min_val=None, max_val=None):
    while True:
        try:
            value = int(input(prompt))
            if min_val is not None and value < min_val:
                print(f"Must be at least {min_val}")
                continue
            if max_val is not None and value > max_val:
                print(f"Must be at most {max_val}")
                continue
            return value
        except ValueError:
            print("Please enter a valid number.")

age = get_integer("Enter your age: ", min_val=0, max_val=150)

Safe File Reader

def read_config(filename):
    try:
        with open(filename, "r") as file:
            config = {}
            for line in file:
                line = line.strip()
                if "=" in line and not line.startswith("#"):
                    key, value = line.split("=", 1)
                    config[key.strip()] = value.strip()
            return config
    except FileNotFoundError:
        print(f"Config file '{filename}' not found, using defaults.")
        return {}
    except PermissionError:
        print(f"No permission to read '{filename}'.")
        return {}

settings = read_config("app.conf")

Summary

  • try/except catches and handles exceptions instead of crashing
  • Always catch specific exception types, not bare except:
  • else runs on success; finally always runs (cleanup)
  • raise throws exceptions; create custom classes for domain-specific errors
  • Use as e to access the error message and details
  • Common types: ValueError, TypeError, FileNotFoundError, KeyError, IndexError
🎉
Error handling mastered!

Your programs are now resilient to unexpected inputs and failures. Next up: classes and objects — the foundation of object-oriented programming in Python.