Your validation function checks if age is negative. You need to signal "this is wrong" to the caller. raise creates and throws an exception. The caller must handle it or let it propagate up.

Raise for invalid input

Signal that input doesn't meet requirements.

invalid_input.py
# Raising exceptions on invalid input

def main():
    # Raise on invalid input
    def check_age(age):
        if age < 0:
            raise ValueError("Age cannot be negative")
        if age > 150:
            raise ValueError("Age is unrealistic")
        return age
    
    print("Age validation:")
    
    # Valid age
    try:
        result = check_age(25)
        print(f"  Age {result}: Valid")
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Invalid age
    try:
        result = check_age(-5)
        print(f"  Age {result}: Valid")
    except ValueError as e:
        print(f"  Error: {e}")
    
    
    # Raise on division by zero
    def safe_divide(a, b):
        if b == 0:
            raise ZeroDivisionError("Cannot divide by zero")
        return a / b
    
    print("\nDivision:")
    
    try:
        result = safe_divide(10, 2)
        print(f"  10 / 2 = {result}")
    except ZeroDivisionError as e:
        print(f"  Error: {e}")
    
    try:
        result = safe_divide(10, 0)
        print(f"  10 / 0 = {result}")
    except ZeroDivisionError as e:
        print(f"  Error: {e}")
    
    # Validate string input
    def validate_username(username):
        if not username:
            raise ValueError("Username cannot be empty")
        if len(username) < 3:
            raise ValueError("Username must be at least 3 characters")
        if not username.isalnum():
            raise ValueError("Username must be alphanumeric")
        return username
    
    print("\nUsername validation:")
    
    usernames = 
    
    for name in usernames:
        try:
            valid = validate_username(name)
            print(f"  '{name}': OK")
        except ValueError as e:
            print(f"  '{name}': {e}")
    
if __name__ == "__main__":
    main()
# Raising exceptions on invalid input

def main():
    # Raise on invalid input
    def check_age(age):
        if age < 0:
            raise ValueError("Age cannot be negative")
        if age > 150:
            raise ValueError("Age is unrealistic")
        return age
    
    print("Age validation:")
    
    # Valid age
    try:
        result = check_age(25)
        print(f"  Age {result}: Valid")
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Invalid age
    try:
        result = check_age(-5)
        print(f"  Age {result}: Valid")
    except ValueError as e:
        print(f"  Error: {e}")
    
    
    # Raise on division by zero
    def safe_divide(a, b):
        if b == 0:
            raise ZeroDivisionError("Cannot divide by zero")
        return a / b
    
    print("\nDivision:")
    
    try:
        result = safe_divide(10, 2)
        print(f"  10 / 2 = {result}")
    except ZeroDivisionError as e:
        print(f"  Error: {e}")
    
    try:
        result = safe_divide(10, 0)
        print(f"  10 / 0 = {result}")
    except ZeroDivisionError as e:
        print(f"  Error: {e}")
    
    # Validate string input
    def validate_username(username):
        if not username:
            raise ValueError("Username cannot be empty")
        if len(username) < 3:
            raise ValueError("Username must be at least 3 characters")
        if not username.isalnum():
            raise ValueError("Username must be alphanumeric")
        return username
    
    print("\nUsername validation:")
    
    usernames = 
    
    for name in usernames:
        try:
            valid = validate_username(name)
            print(f"  '{name}': OK")
        except ValueError as e:
            print(f"  '{name}': {e}")
    
if __name__ == "__main__":
    main()
# Raising exceptions on invalid input

def main():
    # Raise on invalid input
    def check_age(age):
        if age < 0:
            raise ValueError("Age cannot be negative")
        if age > 150:
            raise ValueError("Age is unrealistic")
        return age
    
    print("Age validation:")
    
    # Valid age
    try:
        result = check_age(25)
        print(f"  Age {result}: Valid")
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Invalid age
    try:
        result = check_age(-5)
        print(f"  Age {result}: Valid")
    except ValueError as e:
        print(f"  Error: {e}")
    
    
    # Raise on division by zero
    def safe_divide(a, b):
        if b == 0:
            raise ZeroDivisionError("Cannot divide by zero")
        return a / b
    
    print("\nDivision:")
    
    try:
        result = safe_divide(10, 2)
        print(f"  10 / 2 = {result}")
    except ZeroDivisionError as e:
        print(f"  Error: {e}")
    
    try:
        result = safe_divide(10, 0)
        print(f"  10 / 0 = {result}")
    except ZeroDivisionError as e:
        print(f"  Error: {e}")
    
    # Validate string input
    def validate_username(username):
        if not username:
            raise ValueError("Username cannot be empty")
        if len(username) < 3:
            raise ValueError("Username must be at least 3 characters")
        if not username.isalnum():
            raise ValueError("Username must be alphanumeric")
        return username
    
    print("\nUsername validation:")
    
    usernames = 
    
    for name in usernames:
        try:
            valid = validate_username(name)
            print(f"  '{name}': OK")
        except ValueError as e:
            print(f"  '{name}': {e}")
    
if __name__ == "__main__":
    main()

raise ValueError("message") stops execution and signals error to caller.

raise Trigger an exception: `raise ValueError("Invalid")`. Stops normal flow.

Exception propagation

Exceptions bubble up until caught.

propagation.py
# Exception propagation

def main():
    # Exception propagates through call stack
    def level3():
        print("    level3: About to raise")
        raise RuntimeError("Error in level3")
    
    def level2():
        print("  level2: Calling level3")
        level3()
        print("  level2: This won't print")
    
    def level1():
        print("level1: Calling level2")
        level2()
        print("level1: This won't print")
    
    print("Propagation demo:\n")
    
    try:
        level1()
    except RuntimeError as e:
        print(f"\nCaught in main: {e}")
    
    
    # Function that lets exception propagate
    def read_config(key):
        config = {"host": "localhost", "port": 8080}
        # KeyError propagates if key missing
        return config[key]
    
    def get_server_url():
        host = read_config("host")
        port = read_config("port")
        return f"http://{host}:{port}"
    
    print("\nConfig reading:")
    
    try:
        url = get_server_url()
        print(f"  URL: {url}")
    except KeyError as e:
        print(f"  Missing config key: {e}")
    
    try:
        timeout = read_config("timeout")  # Missing key
        print(f"  Timeout: {timeout}")
    except KeyError as e:
        print(f"  Missing config key: {e}")
    
    # Catch at different levels
    def process_data(data):
        if not data:
            raise ValueError("Data is empty")
        return data.upper()
    
    def transform(data):
        # Let exception propagate
        return process_data(data)
    
    def handle_request(data):
        try:
            # Catch here
            result = transform(data)
            return f"Success: {result}"
        except ValueError as e:
            return f"Error: {e}"
    
    print("\nRequest handling:")
    print(f"  {handle_request('hello')}")
    print(f"  {handle_request('')}")
    
    # Chain of operations
    def step1(value):
        if value < 0:
            raise ValueError("Step1: Value must be positive")
        return value * 2
    
    def step2(value):
        if value > 100:
            raise ValueError("Step2: Value too large")
        return value + 10
    
    def step3(value):
        if value % 2 != 0:
            raise ValueError("Step3: Value must be even")
        return value / 2
    
    def pipeline(value):
        v1 = step1(value)
        v2 = step2(v1)
        v3 = step3(v2)
        return v3
    
    print("\nPipeline processing:")
    
    test_values = 
    
    for val in test_values:
        try:
            result = pipeline(val)
            print(f"  {val} → {result}")
        except ValueError as e:
            print(f"  {val} → Failed: {e}")
    
if __name__ == "__main__":
    main()
# Exception propagation

def main():
    # Exception propagates through call stack
    def level3():
        print("    level3: About to raise")
        raise RuntimeError("Error in level3")
    
    def level2():
        print("  level2: Calling level3")
        level3()
        print("  level2: This won't print")
    
    def level1():
        print("level1: Calling level2")
        level2()
        print("level1: This won't print")
    
    print("Propagation demo:\n")
    
    try:
        level1()
    except RuntimeError as e:
        print(f"\nCaught in main: {e}")
    
    
    # Function that lets exception propagate
    def read_config(key):
        config = {"host": "localhost", "port": 8080}
        # KeyError propagates if key missing
        return config[key]
    
    def get_server_url():
        host = read_config("host")
        port = read_config("port")
        return f"http://{host}:{port}"
    
    print("\nConfig reading:")
    
    try:
        url = get_server_url()
        print(f"  URL: {url}")
    except KeyError as e:
        print(f"  Missing config key: {e}")
    
    try:
        timeout = read_config("timeout")  # Missing key
        print(f"  Timeout: {timeout}")
    except KeyError as e:
        print(f"  Missing config key: {e}")
    
    # Catch at different levels
    def process_data(data):
        if not data:
            raise ValueError("Data is empty")
        return data.upper()
    
    def transform(data):
        # Let exception propagate
        return process_data(data)
    
    def handle_request(data):
        try:
            # Catch here
            result = transform(data)
            return f"Success: {result}"
        except ValueError as e:
            return f"Error: {e}"
    
    print("\nRequest handling:")
    print(f"  {handle_request('hello')}")
    print(f"  {handle_request('')}")
    
    # Chain of operations
    def step1(value):
        if value < 0:
            raise ValueError("Step1: Value must be positive")
        return value * 2
    
    def step2(value):
        if value > 100:
            raise ValueError("Step2: Value too large")
        return value + 10
    
    def step3(value):
        if value % 2 != 0:
            raise ValueError("Step3: Value must be even")
        return value / 2
    
    def pipeline(value):
        v1 = step1(value)
        v2 = step2(v1)
        v3 = step3(v2)
        return v3
    
    print("\nPipeline processing:")
    
    test_values = 
    
    for val in test_values:
        try:
            result = pipeline(val)
            print(f"  {val} → {result}")
        except ValueError as e:
            print(f"  {val} → Failed: {e}")
    
if __name__ == "__main__":
    main()
# Exception propagation

def main():
    # Exception propagates through call stack
    def level3():
        print("    level3: About to raise")
        raise RuntimeError("Error in level3")
    
    def level2():
        print("  level2: Calling level3")
        level3()
        print("  level2: This won't print")
    
    def level1():
        print("level1: Calling level2")
        level2()
        print("level1: This won't print")
    
    print("Propagation demo:\n")
    
    try:
        level1()
    except RuntimeError as e:
        print(f"\nCaught in main: {e}")
    
    
    # Function that lets exception propagate
    def read_config(key):
        config = {"host": "localhost", "port": 8080}
        # KeyError propagates if key missing
        return config[key]
    
    def get_server_url():
        host = read_config("host")
        port = read_config("port")
        return f"http://{host}:{port}"
    
    print("\nConfig reading:")
    
    try:
        url = get_server_url()
        print(f"  URL: {url}")
    except KeyError as e:
        print(f"  Missing config key: {e}")
    
    try:
        timeout = read_config("timeout")  # Missing key
        print(f"  Timeout: {timeout}")
    except KeyError as e:
        print(f"  Missing config key: {e}")
    
    # Catch at different levels
    def process_data(data):
        if not data:
            raise ValueError("Data is empty")
        return data.upper()
    
    def transform(data):
        # Let exception propagate
        return process_data(data)
    
    def handle_request(data):
        try:
            # Catch here
            result = transform(data)
            return f"Success: {result}"
        except ValueError as e:
            return f"Error: {e}"
    
    print("\nRequest handling:")
    print(f"  {handle_request('hello')}")
    print(f"  {handle_request('')}")
    
    # Chain of operations
    def step1(value):
        if value < 0:
            raise ValueError("Step1: Value must be positive")
        return value * 2
    
    def step2(value):
        if value > 100:
            raise ValueError("Step2: Value too large")
        return value + 10
    
    def step3(value):
        if value % 2 != 0:
            raise ValueError("Step3: Value must be even")
        return value / 2
    
    def pipeline(value):
        v1 = step1(value)
        v2 = step2(v1)
        v3 = step3(v2)
        return v3
    
    print("\nPipeline processing:")
    
    test_values = 
    
    for val in test_values:
        try:
            result = pipeline(val)
            print(f"  {val} → {result}")
        except ValueError as e:
            print(f"  {val} → Failed: {e}")
    
if __name__ == "__main__":
    main()

Uncaught exception travels up the call stack to the first handler.

Include error details

Provide useful information in the exception message.

with_message.py
# Raising exceptions with messages

def main():
    # Detailed error messages
    def withdraw(balance, amount):
        if amount <= 0:
            raise ValueError(f"Withdrawal amount must be positive, got {amount}")
        if amount > balance:
            raise ValueError(f"Insufficient funds: balance={balance}, requested={amount}")
        return balance - amount
    
    print("Bank withdrawals:\n")
    
    balance = 
    
    # Successful withdrawal
    try:
        new_balance = withdraw(balance, 30)
        print(f"  Withdrew $30: New balance ${new_balance}")
        balance = new_balance
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Invalid amount
    try:
        new_balance = withdraw(balance, -10)
        print(f"  Withdrew $-10: New balance ${new_balance}")
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Insufficient funds
    try:
        new_balance = withdraw(balance, 200)
        print(f"  Withdrew $200: New balance ${new_balance}")
    except ValueError as e:
        print(f"  Error: {e}")
    
    
    # Type-specific error messages
    def calculate_discount(price, discount_percent):
        if not isinstance(price, (int, float)):
            raise TypeError(f"Price must be numeric, got {type(price).__name__}")
        if not isinstance(discount_percent, (int, float)):
            raise TypeError(f"Discount must be numeric, got {type(discount_percent).__name__}")
        if discount_percent < 0 or discount_percent > 100:
            raise ValueError(f"Discount must be 0-100%, got {discount_percent}")
        
        return price * (1 - discount_percent / 100)
    
    print("\nDiscount calculations:")
    
    test_cases = [
        (100, 10),
        (100, "10"),
        ("100", 10),
        (100, 150)
    ]
    
    for price, discount in test_cases:
        try:
            final_price = calculate_discount(price, discount)
            print(f"  ${price} - {discount}% = ${final_price:.2f}")
        except (TypeError, ValueError) as e:
            print(f"  ${price} - {discount}%: {e}")
    
    # Index validation with context
    def get_item(items, index):
        if not isinstance(index, int):
            raise TypeError(f"Index must be integer, got {type(index).__name__}")
        if index < 0:
            raise IndexError(f"Index must be non-negative, got {index}")
        if index >= len(items):
            raise IndexError(f"Index {index} out of range (list size: {len(items)})")
        return items[index]
    
    print("\nList access:")
    
    fruits = ["apple", "banana", "cherry"]
    indices = [0, 5, -1, "2"]
    
    for idx in indices:
        try:
            fruit = get_item(fruits, idx)
            print(f"  fruits[{idx}] = {fruit}")
        except (TypeError, IndexError) as e:
            print(f"  fruits[{idx}]: {e}")
    
if __name__ == "__main__":
    main()
# Raising exceptions with messages

def main():
    # Detailed error messages
    def withdraw(balance, amount):
        if amount <= 0:
            raise ValueError(f"Withdrawal amount must be positive, got {amount}")
        if amount > balance:
            raise ValueError(f"Insufficient funds: balance={balance}, requested={amount}")
        return balance - amount
    
    print("Bank withdrawals:\n")
    
    balance = 
    
    # Successful withdrawal
    try:
        new_balance = withdraw(balance, 30)
        print(f"  Withdrew $30: New balance ${new_balance}")
        balance = new_balance
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Invalid amount
    try:
        new_balance = withdraw(balance, -10)
        print(f"  Withdrew $-10: New balance ${new_balance}")
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Insufficient funds
    try:
        new_balance = withdraw(balance, 200)
        print(f"  Withdrew $200: New balance ${new_balance}")
    except ValueError as e:
        print(f"  Error: {e}")
    
    
    # Type-specific error messages
    def calculate_discount(price, discount_percent):
        if not isinstance(price, (int, float)):
            raise TypeError(f"Price must be numeric, got {type(price).__name__}")
        if not isinstance(discount_percent, (int, float)):
            raise TypeError(f"Discount must be numeric, got {type(discount_percent).__name__}")
        if discount_percent < 0 or discount_percent > 100:
            raise ValueError(f"Discount must be 0-100%, got {discount_percent}")
        
        return price * (1 - discount_percent / 100)
    
    print("\nDiscount calculations:")
    
    test_cases = [
        (100, 10),
        (100, "10"),
        ("100", 10),
        (100, 150)
    ]
    
    for price, discount in test_cases:
        try:
            final_price = calculate_discount(price, discount)
            print(f"  ${price} - {discount}% = ${final_price:.2f}")
        except (TypeError, ValueError) as e:
            print(f"  ${price} - {discount}%: {e}")
    
    # Index validation with context
    def get_item(items, index):
        if not isinstance(index, int):
            raise TypeError(f"Index must be integer, got {type(index).__name__}")
        if index < 0:
            raise IndexError(f"Index must be non-negative, got {index}")
        if index >= len(items):
            raise IndexError(f"Index {index} out of range (list size: {len(items)})")
        return items[index]
    
    print("\nList access:")
    
    fruits = ["apple", "banana", "cherry"]
    indices = [0, 5, -1, "2"]
    
    for idx in indices:
        try:
            fruit = get_item(fruits, idx)
            print(f"  fruits[{idx}] = {fruit}")
        except (TypeError, IndexError) as e:
            print(f"  fruits[{idx}]: {e}")
    
if __name__ == "__main__":
    main()
# Raising exceptions with messages

def main():
    # Detailed error messages
    def withdraw(balance, amount):
        if amount <= 0:
            raise ValueError(f"Withdrawal amount must be positive, got {amount}")
        if amount > balance:
            raise ValueError(f"Insufficient funds: balance={balance}, requested={amount}")
        return balance - amount
    
    print("Bank withdrawals:\n")
    
    balance = 
    
    # Successful withdrawal
    try:
        new_balance = withdraw(balance, 30)
        print(f"  Withdrew $30: New balance ${new_balance}")
        balance = new_balance
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Invalid amount
    try:
        new_balance = withdraw(balance, -10)
        print(f"  Withdrew $-10: New balance ${new_balance}")
    except ValueError as e:
        print(f"  Error: {e}")
    
    # Insufficient funds
    try:
        new_balance = withdraw(balance, 200)
        print(f"  Withdrew $200: New balance ${new_balance}")
    except ValueError as e:
        print(f"  Error: {e}")
    
    
    # Type-specific error messages
    def calculate_discount(price, discount_percent):
        if not isinstance(price, (int, float)):
            raise TypeError(f"Price must be numeric, got {type(price).__name__}")
        if not isinstance(discount_percent, (int, float)):
            raise TypeError(f"Discount must be numeric, got {type(discount_percent).__name__}")
        if discount_percent < 0 or discount_percent > 100:
            raise ValueError(f"Discount must be 0-100%, got {discount_percent}")
        
        return price * (1 - discount_percent / 100)
    
    print("\nDiscount calculations:")
    
    test_cases = [
        (100, 10),
        (100, "10"),
        ("100", 10),
        (100, 150)
    ]
    
    for price, discount in test_cases:
        try:
            final_price = calculate_discount(price, discount)
            print(f"  ${price} - {discount}% = ${final_price:.2f}")
        except (TypeError, ValueError) as e:
            print(f"  ${price} - {discount}%: {e}")
    
    # Index validation with context
    def get_item(items, index):
        if not isinstance(index, int):
            raise TypeError(f"Index must be integer, got {type(index).__name__}")
        if index < 0:
            raise IndexError(f"Index must be non-negative, got {index}")
        if index >= len(items):
            raise IndexError(f"Index {index} out of range (list size: {len(items)})")
        return items[index]
    
    print("\nList access:")
    
    fruits = ["apple", "banana", "cherry"]
    indices = [0, 5, -1, "2"]
    
    for idx in indices:
        try:
            fruit = get_item(fruits, idx)
            print(f"  fruits[{idx}] = {fruit}")
        except (TypeError, IndexError) as e:
            print(f"  fruits[{idx}]: {e}")
    
if __name__ == "__main__":
    main()

Include the problematic value: raise ValueError(f"Invalid age: {age}").

Re-raise current exception

Catch, do something, then let it propagate.

reraise.py
# Re-raising exceptions after logging

def main():
    # Log and re-raise
    def process_payment(amount):
        if amount <= 0:
            raise ValueError("Amount must be positive")
        print(f"    Processing payment: ${amount}")
        return {"status": "success", "amount": amount}
    
    def handle_payment(amount):
        try:
            return process_payment(amount)
        except ValueError as e:
            print(f"  [LOG] Payment failed: {e}")
            raise  # Re-raise the same exception
    
    print("Payment processing:\n")
    
    try:
        result = handle_payment(100)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Main caught: {e}")
    
    print()
    
    try:
        result = handle_payment(-50)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Main caught: {e}")
    
    
    # Cleanup and re-raise
    def database_operation(query):
        connection = "DB_CONNECTION"
        print(f"  Opened: {connection}")
        
        try:
            if "invalid" in query:
                raise RuntimeError(f"Bad query: {query}")
            print(f"  Executed: {query}")
            return "RESULT"
        except RuntimeError:
            print(f"  Error occurred, cleaning up...")
            print(f"  Closed: {connection}")
            raise  # Re-raise after cleanup
        finally:
            # This runs regardless
            pass
    
    print("\nDatabase operations:")
    
    try:
        result = database_operation("SELECT * FROM users")
        print(f"  Success: {result}\n")
    except RuntimeError as e:
        print(f"  Failed: {e}\n")
    
    try:
        result = database_operation("invalid syntax")
        print(f"  Success: {result}")
    except RuntimeError as e:
        print(f"  Failed: {e}")
    
    # Conditional re-raise
    def validate_and_parse(data):
        try:
            value = int(data)
            if value < 0:
                raise ValueError("Value must be non-negative")
            return value
        except ValueError as e:
            if "invalid literal" in str(e):
                # Handle parse errors locally
                print(f"  Parse error, using default")
                return 0
            else:
                # Re-raise validation errors
                raise
    
    print("\nValidation and parsing:")
    
    test_data = 
    
    for data in test_data:
        try:
            result = validate_and_parse(data)
            print(f"  '{data}' → {result}")
        except ValueError as e:
            print(f"  '{data}' → Error: {e}")
    
    # Transform and re-raise
    def risky_operation():
        items = [1, 2, 3]
        return items[10]  # IndexError
    
    def wrapped_operation():
        try:
            return risky_operation()
        except IndexError as e:
            print(f"  [LOG] IndexError: {e}")
            # Could transform exception type here
            raise RuntimeError("Operation failed") from e
    
    print("\nWrapped operation:")
    
    try:
        wrapped_operation()
    except RuntimeError as e:
        print(f"  Caught RuntimeError: {e}")
        print(f"  Caused by: {e.__cause__}")
    
if __name__ == "__main__":
    main()
# Re-raising exceptions after logging

def main():
    # Log and re-raise
    def process_payment(amount):
        if amount <= 0:
            raise ValueError("Amount must be positive")
        print(f"    Processing payment: ${amount}")
        return {"status": "success", "amount": amount}
    
    def handle_payment(amount):
        try:
            return process_payment(amount)
        except ValueError as e:
            print(f"  [LOG] Payment failed: {e}")
            raise  # Re-raise the same exception
    
    print("Payment processing:\n")
    
    try:
        result = handle_payment(100)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Main caught: {e}")
    
    print()
    
    try:
        result = handle_payment(-50)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Main caught: {e}")
    
    
    # Cleanup and re-raise
    def database_operation(query):
        connection = "DB_CONNECTION"
        print(f"  Opened: {connection}")
        
        try:
            if "invalid" in query:
                raise RuntimeError(f"Bad query: {query}")
            print(f"  Executed: {query}")
            return "RESULT"
        except RuntimeError:
            print(f"  Error occurred, cleaning up...")
            print(f"  Closed: {connection}")
            raise  # Re-raise after cleanup
        finally:
            # This runs regardless
            pass
    
    print("\nDatabase operations:")
    
    try:
        result = database_operation("SELECT * FROM users")
        print(f"  Success: {result}\n")
    except RuntimeError as e:
        print(f"  Failed: {e}\n")
    
    try:
        result = database_operation("invalid syntax")
        print(f"  Success: {result}")
    except RuntimeError as e:
        print(f"  Failed: {e}")
    
    # Conditional re-raise
    def validate_and_parse(data):
        try:
            value = int(data)
            if value < 0:
                raise ValueError("Value must be non-negative")
            return value
        except ValueError as e:
            if "invalid literal" in str(e):
                # Handle parse errors locally
                print(f"  Parse error, using default")
                return 0
            else:
                # Re-raise validation errors
                raise
    
    print("\nValidation and parsing:")
    
    test_data = 
    
    for data in test_data:
        try:
            result = validate_and_parse(data)
            print(f"  '{data}' → {result}")
        except ValueError as e:
            print(f"  '{data}' → Error: {e}")
    
    # Transform and re-raise
    def risky_operation():
        items = [1, 2, 3]
        return items[10]  # IndexError
    
    def wrapped_operation():
        try:
            return risky_operation()
        except IndexError as e:
            print(f"  [LOG] IndexError: {e}")
            # Could transform exception type here
            raise RuntimeError("Operation failed") from e
    
    print("\nWrapped operation:")
    
    try:
        wrapped_operation()
    except RuntimeError as e:
        print(f"  Caught RuntimeError: {e}")
        print(f"  Caused by: {e.__cause__}")
    
if __name__ == "__main__":
    main()
# Re-raising exceptions after logging

def main():
    # Log and re-raise
    def process_payment(amount):
        if amount <= 0:
            raise ValueError("Amount must be positive")
        print(f"    Processing payment: ${amount}")
        return {"status": "success", "amount": amount}
    
    def handle_payment(amount):
        try:
            return process_payment(amount)
        except ValueError as e:
            print(f"  [LOG] Payment failed: {e}")
            raise  # Re-raise the same exception
    
    print("Payment processing:\n")
    
    try:
        result = handle_payment(100)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Main caught: {e}")
    
    print()
    
    try:
        result = handle_payment(-50)
        print(f"Result: {result}")
    except ValueError as e:
        print(f"Main caught: {e}")
    
    
    # Cleanup and re-raise
    def database_operation(query):
        connection = "DB_CONNECTION"
        print(f"  Opened: {connection}")
        
        try:
            if "invalid" in query:
                raise RuntimeError(f"Bad query: {query}")
            print(f"  Executed: {query}")
            return "RESULT"
        except RuntimeError:
            print(f"  Error occurred, cleaning up...")
            print(f"  Closed: {connection}")
            raise  # Re-raise after cleanup
        finally:
            # This runs regardless
            pass
    
    print("\nDatabase operations:")
    
    try:
        result = database_operation("SELECT * FROM users")
        print(f"  Success: {result}\n")
    except RuntimeError as e:
        print(f"  Failed: {e}\n")
    
    try:
        result = database_operation("invalid syntax")
        print(f"  Success: {result}")
    except RuntimeError as e:
        print(f"  Failed: {e}")
    
    # Conditional re-raise
    def validate_and_parse(data):
        try:
            value = int(data)
            if value < 0:
                raise ValueError("Value must be non-negative")
            return value
        except ValueError as e:
            if "invalid literal" in str(e):
                # Handle parse errors locally
                print(f"  Parse error, using default")
                return 0
            else:
                # Re-raise validation errors
                raise
    
    print("\nValidation and parsing:")
    
    test_data = 
    
    for data in test_data:
        try:
            result = validate_and_parse(data)
            print(f"  '{data}' → {result}")
        except ValueError as e:
            print(f"  '{data}' → Error: {e}")
    
    # Transform and re-raise
    def risky_operation():
        items = [1, 2, 3]
        return items[10]  # IndexError
    
    def wrapped_operation():
        try:
            return risky_operation()
        except IndexError as e:
            print(f"  [LOG] IndexError: {e}")
            # Could transform exception type here
            raise RuntimeError("Operation failed") from e
    
    print("\nWrapped operation:")
    
    try:
        wrapped_operation()
    except RuntimeError as e:
        print(f"  Caught RuntimeError: {e}")
        print(f"  Caused by: {e.__cause__}")
    
if __name__ == "__main__":
    main()

Bare raise in except block re-raises the current exception.

Exception chaining

Preserve original exception when raising a new one.

exception_chaining.py
# Exception chaining with 'from'

def main():
    # Chain exceptions with 'from'
    def load_config():
        config_data = {"port": "invalid"}
        
        try:
            port = int(config_data["port"])
            return port
        except ValueError as e:
            # Chain new exception from original
            raise RuntimeError("Config load failed") from e
    
    print("Exception chaining:\n")
    
    try:
        port = load_config()
        print(f"Port: {port}")
    except RuntimeError as e:
        print(f"Exception: {e}")
        print(f"Caused by: {e.__cause__}")
        print(f"Cause type: {type(e.__cause__).__name__}")
    
    
    # Multiple layers of chaining
    def parse_number(text):
        try:
            return int(text)
        except ValueError as e:
            raise TypeError("Not a number") from e
    
    def process_input(text):
        try:
            num = parse_number(text)
            return num * 2
        except TypeError as e:
            raise RuntimeError("Processing failed") from e
    
    print("\nMulti-layer chaining:")
    
    try:
        result = process_input("abc")
        print(f"Result: {result}")
    except RuntimeError as e:
        print(f"Final exception: {e}")
        print(f"Direct cause: {e.__cause__}")
        print(f"Root cause: {e.__cause__.__cause__}")
    
    # Suppress exception chaining
    def operation_with_fallback():
        try:
            result = 10 / 0
        except ZeroDivisionError:
            # Suppress chain with 'from None'
            raise ValueError("Invalid operation") from None
    
    print("\nSuppressed chain:")
    
    try:
        operation_with_fallback()
    except ValueError as e:
        print(f"Exception: {e}")
        print(f"Has cause: {e.__cause__ is not None}")
    
    # Practical example: data pipeline
    def fetch_data(source):
        if source == "database":
            raise ConnectionError("Database unreachable")
        return None
    
    def transform_data(data):
        if data is None:
            raise ValueError("No data to transform")
        return data.upper()
    
    def save_results(data):
        if not data:
            raise IOError("Cannot save empty data")
        return "SAVED"
    
    def run_pipeline(source):
        try:
            try:
                data = fetch_data(source)
            except ConnectionError as e:
                raise RuntimeError("Fetch stage failed") from e
            
            try:
                transformed = transform_data(data)
            except ValueError as e:
                raise RuntimeError("Transform stage failed") from e
            
            try:
                result = save_results(transformed)
            except IOError as e:
                raise RuntimeError("Save stage failed") from e
            
            return result
        except RuntimeError:
            raise
    
    print("\nPipeline execution:")
    
    try:
        run_pipeline("database")
    except RuntimeError as e:
        print(f"Pipeline failed: {e}")
        if e.__cause__:
            print(f"  Root cause: {type(e.__cause__).__name__}: {e.__cause__}")
    
    # Check exception context
    def analyze_exception(exc):
        print(f"\nException analysis:")
        print(f"  Type: {type(exc).__name__}")
        print(f"  Message: {exc}")
        
        if exc.__cause__:
            print(f"  Has cause: Yes")
            print(f"  Cause: {type(exc.__cause__).__name__}: {exc.__cause__}")
        else:
            print(f"  Has cause: No")
    
    try:
        raise ValueError("Top level") from TypeError("Bottom level")
    except ValueError as e:
        analyze_exception(e)

if __name__ == "__main__":
    main()

raise NewError() from original links exceptions together.

exception chaining `raise X from Y` - new exception X caused by original Y. Full traceback preserved.

Exercise: practical.py

Build a validation system with proper exception raising