Skip to content

Latest commit

 

History

History
614 lines (467 loc) · 13.2 KB

File metadata and controls

614 lines (467 loc) · 13.2 KB

Lab 3.1: Python Exception Handling

Overview

Programs fail. Files don't exist, networks disconnect, users enter bad data. Exception handling lets you gracefully handle errors instead of crashing.

What you'll learn:

  • Try/except basics
  • Common exceptions (ValueError, KeyError, FileNotFoundError, etc.)
  • Multiple exception handlers
  • Generic catch-all exceptions
  • Finally blocks for cleanup
  • Two approaches: validation vs exception handling

Part 1: The Problem - Programs Crash

Step 1: Code That Crashes

# This crashes!
age = int(input("Enter your age: "))
print(f"You are {age} years old")

Try it with bad input:

Enter your age: abc
ValueError: invalid literal for int() with base 10: 'abc'

Program crashed! User sees ugly error message.

Step 2: More Ways to Crash

# KeyError - dictionary key doesn't exist
config = {"host": "localhost", "port": 8080}
print(config["username"])  # KeyError: 'username'

# FileNotFoundError
with open("nonexistent.txt", "r") as f:
    data = f.read()  # FileNotFoundError

# ZeroDivisionError
result = 10 / 0  # ZeroDivisionError

# IndexError
numbers = [1, 2, 3]
print(numbers[10])  # IndexError: list index out of range

Without exception handling, your program stops at the first error.


Part 2: Basic Try/Except

Step 1: Catching Exceptions

# Handle the error gracefully
while True:
    try:
        age = int(input("Enter your age: "))
        print(f"You are {age} years old")
        break
    except:
        print("That's not a valid age!")
        continue

Now try with bad input:

Enter your age: abc
That's not a valid age!

Program continues! User sees friendly message.

Step 2: Named Exceptions

It's better to catch specific exceptions:

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

Step 3: Getting the Error Message

try:
    age = int(input("Enter your age: "))
    print(f"You are {age} years old")
except ValueError as e:
    print(f"Invalid input: {e}")

Output with bad input:

Enter your age: xyz
Invalid input: invalid literal for int() with base 10: 'xyz'

The as e captures the exception object so you can inspect it.


Part 3: Common Exceptions

ValueError - Invalid Type Conversion

# Converting strings to numbers
def get_number():
    try:
        num = int(input("Enter a number: "))
        return num
    except ValueError:
        print("That's not a valid number!")
        return None

result = get_number()
if result is not None:
    print(f"You entered: {result}")

KeyError - Dictionary Key Not Found

# Accessing dictionary keys
device_config = {
    "hostname": "router-1",
    "ip": "192.168.1.1",
    "port": 22
}

# Bad approach - crashes if key missing
try:
    username = device_config["username"]
except KeyError:
    print("Username not found in config")
    username = "admin"  # Use default

print(f"Username: {username}")

Better approach: Use .get() with default value:

username = device_config.get("username", "admin")

FileNotFoundError - File Doesn't Exist

# Reading files
try:
    with open("config.txt", "r") as f:
        data = f.read()
        print(data)
except FileNotFoundError:
    print("Config file not found, using defaults")
    data = "default config"

ZeroDivisionError - Divide by Zero

# Math operations
def safe_divide(a, b):
    try:
        result = a / b
        return result
    except ZeroDivisionError:
        print("Cannot divide by zero!")
        return None

print(safe_divide(10, 2))   # 5.0
print(safe_divide(10, 0))   # Cannot divide by zero! / None

IndexError - List Index Out of Range

# Accessing list elements
servers = ["web-1", "web-2", "web-3"]

try:
    print(servers[5])
except IndexError:
    print("Server index doesn't exist")

Better approach: Check length first or use slicing:

if len(servers) > 5:
    print(servers[5])

TypeError - Wrong Type Used

# Mixing incompatible types
try:
    result = "hello" + 5
except TypeError as e:
    print(f"Type error: {e}")
    # Output: Type error: can only concatenate str (not "int") to str

Part 4: Multiple Exception Handlers

You can handle different exceptions differently:

def read_server_port(filename, server_name):
    """Read server port from config file"""
    try:
        # Open file
        with open(filename, "r") as f:
            import json
            config = json.load(f)
        
        # Get server config
        server = config[server_name]
        
        # Get port
        port = int(server["port"])
        
        return port
    
    except FileNotFoundError:
        print(f"Config file '{filename}' not found")
        return 8080  # Default port
    
    except KeyError as e:
        print(f"Server or key not found: {e}")
        return 8080
    
    except ValueError:
        print("Port is not a valid number")
        return 8080
    
    except json.JSONDecodeError:
        print("Invalid JSON in config file")
        return 8080

# Test it
port = read_server_port("servers.json", "web-1")
print(f"Using port: {port}")

Each exception type gets its own handler!


Part 5: Generic Catch-All Exception

Sometimes you want to catch ANY exception:

def risky_operation():
    try:
        # Multiple things that could fail
        data = fetch_data_from_api()
        process_data(data)
        save_to_database(data)
    
    except ValueError:
        print("Invalid data format")
    
    except KeyError:
        print("Missing required field")
    
    except Exception as e:
        # Catches ANYTHING else
        print(f"Unexpected error: {e}")

Order matters:

  1. Specific exceptions first (ValueError, KeyError)
  2. Generic Exception last (catches everything else)

Bare except (Not Recommended)

try:
    risky_code()
except:
    print("Something went wrong")

Problem: Catches EVERYTHING, including KeyboardInterrupt (Ctrl+C). Hard to debug!

Better: Always name the exception:

except Exception as e:
    print(f"Error: {e}")

Raising Exceptions (When to Fail on Purpose)

The raise keyword is how you say: this is invalid, and someone else must deal with it.

It creates an exception by raising an exception object up the call stack, where it must be caught by a caller; otherwise, program execution will halt.

def validate_port(port):
    if not (1 <= port <= 65535):
        raise ValueError("Port must be between 1 and 65535")
    return port

Part 6: Finally Block - Always Runs

The finally block runs whether an exception occurs or not. Perfect for cleanup!

def read_config(filename):
    file = None
    try:
        file = open(filename, "r")
        data = file.read()
        return data
    
    except FileNotFoundError:
        print(f"File not found: {filename}")
        return None
    
    finally:
        # This ALWAYS runs
        if file:
            file.close()
            print("File closed")

config = read_config("config.txt")

Common use cases for finally:

  • Close files
  • Close network connections
  • Release locks
  • Clean up resources

Note: Using with open() is better - it auto-closes files!

# Preferred approach - auto cleanup
try:
    with open(filename, "r") as f:
        data = f.read()
except FileNotFoundError:
    print("File not found")

Part 7: Validation vs Exception Handling

Two approaches to handling bad input:

Approach 1: Validate Before (LBYL - Look Before You Leap)

def get_age_validated():
    """Check if input is valid before converting"""
    age_input = input("Enter your age: ")
    
    # Validate first
    if age_input.isdigit():
        age = int(age_input)
        if 0 < age < 150:
            return age
        else:
            print("Age must be between 1 and 149")
            return None
    else:
        print("Please enter a number")
        return None

age = get_age_validated()

Pros:

  • Predictable flow
  • No exceptions raised
  • Can provide specific error messages

Cons:

  • More code
  • Still might miss edge cases

Approach 2: Try and Handle Exception (EAFP - Easier to Ask Forgiveness than Permission)

def get_age_exception():
    """Try to convert, handle errors if they occur"""
    try:
        age_input = input("Enter your age: ")
        age = int(age_input)
        
        if not 0 < age < 150:
            raise ValueError("Age must be between 1 and 149")
        
        return age
    
    except ValueError as e:
        print(f"Invalid age: {e}")
        return None

age = get_age_exception()

Pros:

  • More "Pythonic"
  • Handles unexpected errors
  • Less code for checking

Cons:

  • Can hide bugs if too broad
  • Harder to debug if not careful

Python philosophy: EAFP is preferred - "It's easier to ask forgiveness than permission"


Part 10: Hands-On Challenge

Your task: Create a device configuration manager with error handling

Requirements:

  1. Create a program that:

    • Reads device names from a file (devices.txt)
    • For each device, reads its config from configs/<device-name>.json
    • Displays device info (hostname, IP, type)
    • Handles all possible errors gracefully
  2. Handle these exceptions:

    • FileNotFoundError - devices.txt or config file missing
    • json.JSONDecodeError - invalid JSON
    • KeyError - missing required fields
    • Exception - any other unexpected errors
  3. Test with:

    • Missing files
    • Invalid JSON
    • Missing required keys

Starter code:

import json

def load_devices(filename):
    """Load device list with error handling"""
    # TODO: Read devices.txt
    # TODO: Handle FileNotFoundError
    pass

def load_device_config(device_name):
    """Load device configuration with error handling"""
    # TODO: Read configs/<device-name>.json
    # TODO: Handle FileNotFoundError, JSONDecodeError
    pass

def get_device_info(config):
    """Extract device info with error handling"""
    # TODO: Get hostname, ip, type from config
    # TODO: Handle KeyError for missing fields
    pass

def main():
    """Main program"""
    devices = load_devices("devices.txt")
    
    for device_name in devices:
        config = load_device_config(device_name)
        if config:
            info = get_device_info(config)
            print(info)

if __name__ == "__main__":
    main()

Test files:

devices.txt:

router-1
switch-1
firewall-1

configs/router-1.json:

{
    "hostname": "router-1",
    "ip": "192.168.1.1",
    "type": "cisco_ios"
}

Success Criteria

You've completed this lab when you can:

  • Use try/except to handle exceptions
  • Catch specific exceptions (ValueError, KeyError, FileNotFoundError)
  • Use multiple exception handlers
  • Use generic Exception as catch-all
  • Use finally for cleanup
  • Access exception messages with as e
  • Decide between validation (LBYL) vs exceptions (EAFP)
  • Handle common errors in file I/O, dictionaries, and type conversions

Key Takeaways

Exception Handling Basics:

  • try - Code that might fail
  • except SomeError - Handle specific error
  • except Exception - Catch-all for unexpected errors
  • finally - Always runs (cleanup)
  • as e - Capture exception object

Common Exceptions:

  • ValueError - Invalid value for conversion
  • KeyError - Dictionary key not found
  • FileNotFoundError - File doesn't exist
  • ZeroDivisionError - Division by zero
  • IndexError - List index out of range
  • TypeError - Wrong type used

Best Practices:

  • Catch specific exceptions first, generic last
  • Always name exceptions (except Exception as e not bare except)
  • Use with statements for files (auto cleanup)
  • Don't catch exceptions you can't handle
  • Python prefers EAFP (try/except) over LBYL (check first)

When to Use:

  • ✅ File operations (might not exist)
  • ✅ Network operations (might fail)
  • ✅ User input (might be invalid)
  • ✅ Parsing data (might be malformed)
  • ❌ Normal control flow (use if instead)

Real-world applications:

  • Reading configuration files
  • Connecting to network devices
  • Parsing API responses
  • Processing user input
  • Handling network timeouts

Quick Reference

# Basic try/except
try:
    risky_code()
except ValueError:
    handle_error()

# Multiple exceptions
try:
    risky_code()
except ValueError:
    handle_value_error()
except KeyError:
    handle_key_error()
except Exception as e:
    handle_any_other_error(e)

# With finally
try:
    risky_code()
except Exception as e:
    handle_error(e)
finally:
    cleanup()

# Get exception message
except ValueError as e:
    print(f"Error: {e}")

Additional Resources


Congratulations! You now know how to handle errors gracefully and build robust Python programs!