165 lines
5.1 KiB
Python
165 lines
5.1 KiB
Python
# Lesson 09: Error Handling
|
|
#
|
|
# Errors (called exceptions) happen at runtime — bad input, missing files,
|
|
# dividing by zero, etc. Instead of crashing, you can catch and handle them
|
|
# gracefully using try/except blocks.
|
|
#
|
|
# To run this file:
|
|
# python 09_error_handling.py
|
|
|
|
# ============================================================
|
|
# WHY PROGRAMS CRASH
|
|
# ============================================================
|
|
# Uncomment any line below to see the error it produces, then re-comment it.
|
|
|
|
# print(10 / 0) # ZeroDivisionError
|
|
# print(int("hello")) # ValueError
|
|
# print([][5]) # IndexError
|
|
# print({"a": 1}["z"]) # KeyError
|
|
# print(open("nope.txt")) # FileNotFoundError
|
|
|
|
# ============================================================
|
|
# BASIC TRY / EXCEPT
|
|
# ============================================================
|
|
# Code inside 'try' runs normally.
|
|
# If an error occurs, Python jumps to the matching 'except' block
|
|
# instead of crashing.
|
|
|
|
try:
|
|
result = 10 / 0
|
|
except ZeroDivisionError:
|
|
print("Cannot divide by zero!")
|
|
|
|
# The program continues running after the except block.
|
|
print("Program is still running.")
|
|
|
|
# ============================================================
|
|
# CATCHING MULTIPLE EXCEPTION TYPES
|
|
# ============================================================
|
|
|
|
def safe_divide(a, b):
|
|
try:
|
|
return a / b
|
|
except ZeroDivisionError:
|
|
print("Error: division by zero.")
|
|
return None
|
|
except TypeError:
|
|
print("Error: both arguments must be numbers.")
|
|
return None
|
|
|
|
print(safe_divide(10, 2)) # 5.0
|
|
print(safe_divide(10, 0)) # Error message, then None
|
|
print(safe_divide(10, "x")) # Error message, then None
|
|
|
|
# ============================================================
|
|
# THE else AND finally CLAUSES
|
|
# ============================================================
|
|
# else runs only if NO exception occurred.
|
|
# finally always runs, whether or not an exception occurred.
|
|
# Use finally for cleanup (closing files, database connections, etc.).
|
|
|
|
def parse_number(text):
|
|
try:
|
|
number = int(text)
|
|
except ValueError:
|
|
print(f" '{text}' is not a valid integer.")
|
|
else:
|
|
print(f" Parsed successfully: {number}")
|
|
finally:
|
|
print(" (parse attempt complete)") # always runs
|
|
|
|
print("\nParsing '42':")
|
|
parse_number("42")
|
|
|
|
print("\nParsing 'abc':")
|
|
parse_number("abc")
|
|
|
|
# ============================================================
|
|
# GETTING THE ERROR MESSAGE
|
|
# ============================================================
|
|
# Use 'as e' to capture the exception object and read its message.
|
|
|
|
try:
|
|
value = int("not a number")
|
|
except ValueError as e:
|
|
print(f"\nCaught a ValueError: {e}")
|
|
|
|
# ============================================================
|
|
# RAISING EXCEPTIONS
|
|
# ============================================================
|
|
# You can deliberately raise an exception to signal that something
|
|
# went wrong in your own code.
|
|
|
|
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 {age} is out of realistic range.")
|
|
return age
|
|
|
|
try:
|
|
set_age(-5)
|
|
except ValueError as e:
|
|
print(f"\nInvalid age: {e}")
|
|
|
|
try:
|
|
set_age("thirty")
|
|
except TypeError as e:
|
|
print(f"Type error: {e}")
|
|
|
|
# ============================================================
|
|
# PRACTICAL EXAMPLE: ROBUST USER INPUT
|
|
# ============================================================
|
|
# In a real program you'd use input() here. For the demo we
|
|
# simulate the user typing a value.
|
|
|
|
def get_positive_number(value_str):
|
|
try:
|
|
number = float(value_str)
|
|
if number <= 0:
|
|
raise ValueError("Number must be positive.")
|
|
return number
|
|
except ValueError as e:
|
|
print(f"Invalid input: {e}")
|
|
return None
|
|
|
|
test_inputs = ["42", "-3", "0", "abc", "7.5"]
|
|
for val in test_inputs:
|
|
result = get_positive_number(val)
|
|
if result is not None:
|
|
print(f" Accepted: {result}")
|
|
|
|
# ============================================================
|
|
# CUSTOM EXCEPTIONS
|
|
# ============================================================
|
|
# You can create your own exception classes by inheriting from Exception.
|
|
# This is useful when you want callers to catch a specific, named error.
|
|
|
|
class InsufficientFundsError(Exception):
|
|
pass
|
|
|
|
class Wallet:
|
|
def __init__(self, balance):
|
|
self.balance = balance
|
|
|
|
def spend(self, amount):
|
|
if amount > self.balance:
|
|
raise InsufficientFundsError(
|
|
f"Tried to spend ${amount:.2f} but only have ${self.balance:.2f}."
|
|
)
|
|
self.balance -= amount
|
|
print(f"Spent ${amount:.2f}. Remaining: ${self.balance:.2f}")
|
|
|
|
wallet = Wallet(50)
|
|
try:
|
|
wallet.spend(30)
|
|
wallet.spend(30) # this will fail
|
|
except InsufficientFundsError as e:
|
|
print(f"\nPayment failed: {e}")
|
|
|
|
# --- Try it yourself ---
|
|
# Write a function 'safe_index(lst, i)' that:
|
|
# - Returns lst[i] if i is a valid index
|
|
# - Catches IndexError and prints a friendly message instead of crashing
|
|
# - Returns None when the index is out of range
|