Python Exceptions

Master error handling and create robust Python applications

🛡️ Exception Handling

Exceptions are Python's way of handling errors that occur during program execution. Instead of crashing, your program can catch these errors and handle them gracefully, making your applications more robust and user-friendly.


# Basic exception handling
try:
    number = int(input("Enter a number: "))
    result = 10 / number
    print(f"Result: {result}")
except ValueError:
    print("Please enter a valid number!")
except ZeroDivisionError:
    print("Cannot divide by zero!")
                                    
60+
Built-in Types
Graceful
Error Handling
Robust
Applications

Exception Categories

🔢

Arithmetic Errors

Math operation problems

ZeroDivisionError
OverflowError
FloatingPointError
📝

Type Errors

Wrong data type usage

TypeError
ValueError
AttributeError
📋

Lookup Errors

Item not found issues

IndexError
KeyError
NameError
📁

System Errors

OS and file problems

FileNotFoundError
PermissionError
OSError

🔧 Basic Exception Handling

The foundation of error handling in Python

🔹 Try-Except Block

# Basic try-except
try:
    age = int(input("Enter your age: "))
    print(f"You are {age} years old")
except ValueError:
    print("Please enter a valid number")

# Handle multiple exceptions
try:
    numbers = [1, 2, 3]
    index = int(input("Enter index: "))
    print(f"Number at index {index}: {numbers[index]}")
except ValueError:
    print("Index must be a number")
except IndexError:
    print("Index out of range")

# Catch multiple exceptions together
try:
    data = {"name": "Alice", "age": 25}
    key = input("Enter key: ")
    print(data[key])
except (KeyError, TypeError) as e:
    print(f"Error accessing data: {e}")

🔹 Else and Finally

# else - runs if no exception occurs
try:
    number = int(input("Enter a number: "))
    result = 100 / number
except ValueError:
    print("Invalid number")
except ZeroDivisionError:
    print("Cannot divide by zero")
else:
    print(f"Result: {result}")
    print("Calculation successful!")

# finally - always runs
def read_file(filename):
    file = None
    try:
        file = open(filename, 'r')
        content = file.read()
        return content
    except FileNotFoundError:
        print(f"File {filename} not found")
        return None
    finally:
        if file:
            file.close()
            print("File closed")

content = read_file("data.txt")

🔹 Exception Information

# Get exception details
try:
    result = 10 / 0
except ZeroDivisionError as e:
    print(f"Error type: {type(e).__name__}")
    print(f"Error message: {e}")
    print(f"Error args: {e.args}")

# Generic exception handler
try:
    # Some risky operation
    data = [1, 2, 3]
    print(data[10])
except Exception as e:
    print(f"An error occurred: {e}")
    print(f"Error type: {type(e).__name__}")

# Import traceback for detailed info
import traceback

try:
    x = 1 / 0
except Exception:
    print("Full traceback:")
    traceback.print_exc()

📋 Common Exception Types

The most frequently encountered exceptions

🔹 Value and Type Errors

# ValueError - wrong value for correct type
try:
    number = int("hello")  # Can't convert to int
except ValueError as e:
    print(f"ValueError: {e}")

try:
    import math
    result = math.sqrt(-1)  # Negative square root
except ValueError as e:
    print(f"Math error: {e}")

# TypeError - wrong type
try:
    result = "hello" + 5  # Can't add string and int
except TypeError as e:
    print(f"TypeError: {e}")

try:
    numbers = [1, 2, 3]
    numbers.append()  # Missing required argument
except TypeError as e:
    print(f"Function error: {e}")

🔹 Index and Key Errors

# IndexError - list index out of range
try:
    fruits = ["apple", "banana", "cherry"]
    print(fruits[5])  # Index 5 doesn't exist
except IndexError as e:
    print(f"IndexError: {e}")

# KeyError - dictionary key doesn't exist
try:
    person = {"name": "Alice", "age": 25}
    print(person["height"])  # Key doesn't exist
except KeyError as e:
    print(f"KeyError: {e}")

# Safe dictionary access
person = {"name": "Alice", "age": 25}
height = person.get("height", "Unknown")
print(f"Height: {height}")  # Returns "Unknown"

🔹 Attribute and Name Errors

# AttributeError - object has no attribute
try:
    text = "hello"
    text.append("world")  # Strings don't have append
except AttributeError as e:
    print(f"AttributeError: {e}")

# NameError - variable not defined
try:
    print(undefined_variable)  # Variable doesn't exist
except NameError as e:
    print(f"NameError: {e}")

# Check if attribute exists
text = "hello"
if hasattr(text, 'upper'):
    print(text.upper())
else:
    print("No upper method")

🔹 File and System Errors

# FileNotFoundError
try:
    with open("nonexistent.txt", "r") as file:
        content = file.read()
except FileNotFoundError as e:
    print(f"File error: {e}")

# PermissionError
try:
    with open("/root/secret.txt", "w") as file:
        file.write("data")
except PermissionError as e:
    print(f"Permission error: {e}")

# Safe file operations
import os

def safe_read_file(filename):
    if os.path.exists(filename):
        try:
            with open(filename, "r") as file:
                return file.read()
        except PermissionError:
            return "Permission denied"
    else:
        return "File not found"

content = safe_read_file("data.txt")
print(content)

🚀 Raising Exceptions

Create and throw your own exceptions

🔹 Raise Built-in Exceptions

# Raise ValueError
def calculate_square_root(number):
    if number < 0:
        raise ValueError("Cannot calculate square root of negative number")
    return number ** 0.5

try:
    result = calculate_square_root(-4)
except ValueError as e:
    print(f"Error: {e}")

# Raise TypeError
def add_numbers(a, b):
    if not isinstance(a, (int, float)) or not isinstance(b, (int, float)):
        raise TypeError("Both arguments must be numbers")
    return a + b

try:
    result = add_numbers("5", 3)
except TypeError as e:
    print(f"Error: {e}")

# Re-raise exception
def process_data(data):
    try:
        return int(data)
    except ValueError:
        print("Logging error...")
        raise  # Re-raise the same exception

🔹 Custom Exceptions

# Create custom exception
class CustomError(Exception):
    """Custom exception for specific errors"""
    pass

class AgeError(Exception):
    """Exception for invalid age values"""
    def __init__(self, age, message="Invalid age"):
        self.age = age
        self.message = message
        super().__init__(self.message)

# Use custom exceptions
def validate_age(age):
    if age < 0:
        raise AgeError(age, "Age cannot be negative")
    if age > 150:
        raise AgeError(age, "Age seems unrealistic")
    return True

try:
    validate_age(-5)
except AgeError as e:
    print(f"Age validation error: {e}")
    print(f"Invalid age was: {e.age}")

# Exception hierarchy
class ValidationError(Exception):
    """Base validation exception"""
    pass

class EmailError(ValidationError):
    """Email validation error"""
    pass

class PasswordError(ValidationError):
    """Password validation error"""
    pass

def validate_email(email):
    if "@" not in email:
        raise EmailError("Email must contain @")

try:
    validate_email("invalid-email")
except ValidationError as e:  # Catches all validation errors
    print(f"Validation failed: {e}")

🔍 Exception Best Practices

Write better exception handling code

🔹 Specific Exception Handling

# Bad: Too broad
try:
    data = process_user_input()
    result = calculate(data)
    save_result(result)
except Exception:  # Catches everything!
    print("Something went wrong")

# Good: Specific exceptions
try:
    data = process_user_input()
    result = calculate(data)
    save_result(result)
except ValueError as e:
    print(f"Invalid input: {e}")
except ZeroDivisionError as e:
    print(f"Math error: {e}")
except FileNotFoundError as e:
    print(f"File error: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")

# Better: Handle at appropriate level
def safe_divide(a, b):
    """Safely divide two numbers"""
    try:
        return a / b
    except ZeroDivisionError:
        return None

def process_numbers(numbers):
    """Process list of number pairs"""
    results = []
    for a, b in numbers:
        result = safe_divide(a, b)
        if result is not None:
            results.append(result)
        else:
            print(f"Cannot divide {a} by {b}")
    return results

🔹 Context Managers

# File handling with context manager
def read_config(filename):
    """Safely read configuration file"""
    try:
        with open(filename, 'r') as file:
            return file.read()
    except FileNotFoundError:
        print(f"Config file {filename} not found")
        return None
    except PermissionError:
        print(f"No permission to read {filename}")
        return None

# Custom context manager
class DatabaseConnection:
    def __init__(self, db_name):
        self.db_name = db_name
        self.connection = None
    
    def __enter__(self):
        print(f"Connecting to {self.db_name}")
        # Simulate connection
        self.connection = f"Connected to {self.db_name}"
        return self.connection
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        print(f"Closing connection to {self.db_name}")
        if exc_type:
            print(f"Exception occurred: {exc_val}")
        return False  # Don't suppress exceptions

# Use custom context manager
try:
    with DatabaseConnection("mydb") as conn:
        print(f"Using {conn}")
        # Simulate error
        raise ValueError("Database error")
except ValueError as e:
    print(f"Handled error: {e}")

🔹 Logging Exceptions

# Proper exception logging
import logging

# Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)

def process_data(data):
    """Process data with proper logging"""
    try:
        # Simulate processing
        if not data:
            raise ValueError("Empty data provided")
        
        result = len(data) * 2
        logging.info(f"Successfully processed data: {result}")
        return result
        
    except ValueError as e:
        logging.error(f"Data validation error: {e}")
        raise
    except Exception as e:
        logging.exception("Unexpected error in process_data")
        raise

# Test with logging
try:
    result = process_data("")
except ValueError:
    print("Handled validation error")

try:
    result = process_data("hello")
    print(f"Result: {result}")
except Exception:
    print("Handled unexpected error")

📚 Exception Hierarchy

Understanding Python's exception structure

🏗️ Exception Hierarchy:

  • BaseException - Root of all exceptions
  • Exception - Base for most exceptions
  • ArithmeticError - Math errors
  • ZeroDivisionError
  • LookupError - Lookup failures
  • IndexError , KeyError
  • ValueError , TypeError
  • SystemExit - Program exit
  • KeyboardInterrupt - Ctrl+C
# Exception hierarchy in action
def demonstrate_hierarchy():
    """Show how exception hierarchy works"""
    
    # Catch specific exception
    try:
        numbers = [1, 2, 3]
        print(numbers[10])
    except IndexError:
        print("Caught IndexError specifically")
    
    # Catch parent exception
    try:
        numbers = [1, 2, 3]
        print(numbers[10])
    except LookupError:  # Parent of IndexError
        print("Caught LookupError (parent)")
    
    # Catch grandparent exception
    try:
        numbers = [1, 2, 3]
        print(numbers[10])
    except Exception:  # Grandparent
        print("Caught Exception (grandparent)")

demonstrate_hierarchy()

# Multiple exception levels
try:
    # This could raise various exceptions
    data = {"numbers": [1, 2, 3]}
    key = input("Enter key: ")
    index = int(input("Enter index: "))
    result = data[key][index]
    print(f"Result: {result}")
    
except KeyError:
    print("Dictionary key not found")
except IndexError:
    print("List index out of range")
except ValueError:
    print("Invalid number format")
except LookupError:  # Catches KeyError and IndexError
    print("General lookup error")
except Exception as e:
    print(f"Other error: {e}")

# Check exception relationships
print(f"Is IndexError a LookupError? {issubclass(IndexError, LookupError)}")
print(f"Is ValueError an Exception? {issubclass(ValueError, Exception)}")
print(f"Is Exception a BaseException? {issubclass(Exception, BaseException)}")

🧠 Test Your Knowledge

Which exception is raised when dividing by zero?

What block always executes in exception handling?

Which keyword is used to throw an exception?