example-projects/reference/python/09_error_handling.py

165 lines
5.1 KiB
Python
Raw Permalink Normal View History

# 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