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
# 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.
# 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 rangeWithout exception handling, your program stops at the first error.
# 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!")
continueNow try with bad input:
Enter your age: abc
That's not a valid age!
Program continues! User sees friendly message.
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!")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.
# 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}")# 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")# 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"# 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# 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])# 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 strYou 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!
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:
- Specific exceptions first (ValueError, KeyError)
- Generic
Exceptionlast (catches everything else)
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}")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 portThe 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")Two approaches to handling bad input:
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
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"
Your task: Create a device configuration manager with error handling
Requirements:
-
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
- Reads device names from a file (
-
Handle these exceptions:
FileNotFoundError- devices.txt or config file missingjson.JSONDecodeError- invalid JSONKeyError- missing required fieldsException- any other unexpected errors
-
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"
}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
Exceptionas catch-all - Use
finallyfor 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
Exception Handling Basics:
try- Code that might failexcept SomeError- Handle specific errorexcept Exception- Catch-all for unexpected errorsfinally- Always runs (cleanup)as e- Capture exception object
Common Exceptions:
ValueError- Invalid value for conversionKeyError- Dictionary key not foundFileNotFoundError- File doesn't existZeroDivisionError- Division by zeroIndexError- List index out of rangeTypeError- Wrong type used
Best Practices:
- Catch specific exceptions first, generic last
- Always name exceptions (
except Exception as enot bareexcept) - Use
withstatements 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
ifinstead)
Real-world applications:
- Reading configuration files
- Connecting to network devices
- Parsing API responses
- Processing user input
- Handling network timeouts
# 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}")Congratulations! You now know how to handle errors gracefully and build robust Python programs!