python-by-example/07_error_handling/01_try_except.py
2025-12-30 08:50:00 +02:00

336 lines
9.9 KiB
Python

"""
================================================================================
File: 01_try_except.py
Topic: Exception Handling with try-except
================================================================================
This file demonstrates error handling in Python using try-except blocks.
Proper error handling makes your code more robust and user-friendly by
gracefully managing unexpected situations.
Key Concepts:
- try, except, else, finally blocks
- Catching specific exceptions
- Exception hierarchy
- Raising exceptions
- Getting exception information
================================================================================
"""
# -----------------------------------------------------------------------------
# 1. Basic try-except
# -----------------------------------------------------------------------------
# Catch and handle errors that would otherwise crash your program
print("--- Basic try-except ---")
# Without error handling - this would crash:
# result = 10 / 0 # ZeroDivisionError!
# With error handling:
try:
result = 10 / 0
except:
print("An error occurred!")
# Better - catch specific exception:
try:
result = 10 / 0
except ZeroDivisionError:
print("Cannot divide by zero!")
result = 0
print(f"Result: {result}")
# -----------------------------------------------------------------------------
# 2. Catching Specific Exceptions
# -----------------------------------------------------------------------------
# Different errors need different handling
print("\n--- Catching Specific Exceptions ---")
def safe_divide(a, b):
"""Divide with specific error handling."""
try:
result = a / b
except ZeroDivisionError:
print(" Error: Cannot divide by zero")
return None
except TypeError:
print(" Error: Invalid types for division")
return None
return result
print(f"10 / 2 = {safe_divide(10, 2)}")
print(f"10 / 0 = {safe_divide(10, 0)}")
print(f"10 / 'a' = {safe_divide(10, 'a')}")
# -----------------------------------------------------------------------------
# 3. Catching Multiple Exceptions
# -----------------------------------------------------------------------------
print("\n--- Multiple Exceptions ---")
# Catch multiple in one line
def process_number(value):
"""Process a number with multiple exception handlers."""
try:
# This might raise ValueError or TypeError
number = int(value)
result = 100 / number
return result
except (ValueError, TypeError) as e:
print(f" Conversion error: {e}")
return None
except ZeroDivisionError:
print(" Cannot divide by zero")
return None
print(f"process_number('5'): {process_number('5')}")
print(f"process_number('abc'): {process_number('abc')}")
print(f"process_number('0'): {process_number('0')}")
# -----------------------------------------------------------------------------
# 4. The else Clause
# -----------------------------------------------------------------------------
# Runs only if no exception occurred
print("\n--- The else Clause ---")
def divide_with_else(a, b):
"""Division with else clause for success logging."""
try:
result = a / b
except ZeroDivisionError:
print(" Division failed: Cannot divide by zero")
return None
else:
# Only runs if try block succeeded
print(f" Division successful: {a} / {b} = {result}")
return result
divide_with_else(10, 2)
divide_with_else(10, 0)
# -----------------------------------------------------------------------------
# 5. The finally Clause
# -----------------------------------------------------------------------------
# Always runs, regardless of whether an exception occurred
print("\n--- The finally Clause ---")
def read_file_example(filename):
"""Demonstrate finally for cleanup."""
file = None
try:
print(f" Attempting to open '{filename}'...")
# Simulating file operation
if filename == "missing.txt":
raise FileNotFoundError("File not found")
print(" File opened successfully!")
return "File content"
except FileNotFoundError as e:
print(f" Error: {e}")
return None
finally:
# This ALWAYS runs
print(" Cleanup: Closing file (if open)")
read_file_example("data.txt")
print()
read_file_example("missing.txt")
# Real-world pattern with file
print("\n--- Real file handling ---")
# Best practice: use 'with' statement (handles cleanup automatically)
# But for learning, here's the try-finally pattern:
"""
file = None
try:
file = open("data.txt", "r")
content = file.read()
finally:
if file:
file.close() # Always closes, even if error occurred
"""
# -----------------------------------------------------------------------------
# 6. Complete try-except-else-finally
# -----------------------------------------------------------------------------
print("\n--- Complete Pattern ---")
def complete_example(value):
"""Show all four clauses in action."""
print(f"\n Processing: {value}")
try:
number = int(value)
result = 100 / number
except ValueError:
print(" EXCEPT: Not a valid integer")
result = None
except ZeroDivisionError:
print(" EXCEPT: Division by zero")
result = None
else:
print(f" ELSE: Success! Result = {result}")
finally:
print(" FINALLY: This always runs")
return result
complete_example("10")
complete_example("abc")
complete_example("0")
# -----------------------------------------------------------------------------
# 7. Getting Exception Information
# -----------------------------------------------------------------------------
print("\n--- Exception Information ---")
try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError as e:
print(f"Exception type: {type(e).__name__}")
print(f"Exception message: {e}")
print(f"Exception args: {e.args}")
# Getting full traceback
import traceback
try:
result = 1 / 0
except ZeroDivisionError:
print("\nFull traceback:")
traceback.print_exc()
# -----------------------------------------------------------------------------
# 8. Common Built-in Exceptions
# -----------------------------------------------------------------------------
print("\n--- Common Exceptions ---")
exceptions_demo = [
("ValueError", "int('abc')"),
("TypeError", "'2' + 2"),
("IndexError", "[1,2,3][10]"),
("KeyError", "{}['missing']"),
("AttributeError", "'string'.missing_method()"),
("FileNotFoundError", "open('nonexistent.txt')"),
("ZeroDivisionError", "1/0"),
("ImportError", "import nonexistent_module"),
("NameError", "undefined_variable"),
]
print("Common exception types:")
for name, example in exceptions_demo:
print(f" {name}: {example}")
# -----------------------------------------------------------------------------
# 9. Exception Hierarchy
# -----------------------------------------------------------------------------
print("\n--- Exception Hierarchy ---")
# All exceptions inherit from BaseException
# Most use Exception as base
hierarchy = """
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── StopIteration
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── FloatingPointError
│ └── OverflowError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── ValueError
├── TypeError
├── AttributeError
├── OSError
│ ├── FileNotFoundError
│ └── PermissionError
└── RuntimeError
"""
print("Exception hierarchy (simplified):")
print(hierarchy)
# Catching parent catches all children
try:
result = 1 / 0
except ArithmeticError: # Catches ZeroDivisionError too!
print("Caught an arithmetic error (parent class)")
# -----------------------------------------------------------------------------
# 10. Practical Examples
# -----------------------------------------------------------------------------
print("\n--- Practical Examples ---")
# Example 1: User input validation
def get_positive_number(prompt):
"""Get a positive number from user (simulated)."""
test_inputs = ["abc", "-5", "0", "10"]
for user_input in test_inputs:
print(f" Input: '{user_input}'", end="")
try:
number = float(user_input)
if number <= 0:
raise ValueError("Number must be positive")
print(f"Valid! ({number})")
return number
except ValueError as e:
print(f"Invalid: {e}")
return None
print("User input validation:")
get_positive_number("Enter a positive number: ")
# Example 2: Safe dictionary access
def safe_get(dictionary, *keys, default=None):
"""Safely navigate nested dictionary."""
current = dictionary
try:
for key in keys:
current = current[key]
return current
except (KeyError, TypeError):
return default
data = {"user": {"profile": {"name": "Alice"}}}
print(f"\nNested access: {safe_get(data, 'user', 'profile', 'name')}")
print(f"Missing key: {safe_get(data, 'user', 'missing', 'name', default='N/A')}")
# Example 3: Retry pattern
def fetch_with_retry(max_retries=3):
"""Simulate fetching with retry on failure."""
import random
for attempt in range(1, max_retries + 1):
try:
print(f" Attempt {attempt}...", end=" ")
# Simulate random failure
if random.random() < 0.7: # 70% chance of failure
raise ConnectionError("Network error")
print("Success!")
return "Data"
except ConnectionError as e:
print(f"Failed: {e}")
if attempt == max_retries:
print(" All retries exhausted!")
return None
print("\nRetry pattern:")
fetch_with_retry()