Bài 14: Exception Handling - Xử Lý Lỗi (Phần 1)

Mục Tiêu Bài Học

Sau khi hoàn thành bài này, bạn sẽ:

  • ✅ Hiểu exceptions và errors trong Python
  • ✅ Sử dụng try/except để handle errors
  • ✅ Làm việc với finally và else clauses
  • ✅ Raise exceptions
  • ✅ Biết các built-in exception types

Exceptions Là Gì?

Exception là error xảy ra khi chạy program, làm program dừng lại.

Errors vs Exceptions

# Syntax Error - code sai cú pháp# if True  # SyntaxError: invalid syntax # Exception - error khi runtimenumber = int("abc")  # ValueError: invalid literal for int() # ZeroDivisionErrorresult = 10 / 0  # ZeroDivisionError: division by zero # NameErrorprint(undefined_variable)  # NameError: name 'undefined_variable' is not defined

Tại Sao Cần Exception Handling?

# ❌ Không handle - program crashesdef divide(a, b):    return a / b result = divide(10, 0)  # Crash!print("This never executes") # ✅ Handle exceptions - program continuesdef divide(a, b):    try:        return a / b    except ZeroDivisionError:        print("Cannot divide by zero!")        return None result = divide(10, 0)  # Handled gracefullyprint("Program continues")  # This executes!

Try/Except - Basic Syntax

Simple Try/Except

# Basic syntaxtry:    # Code that might raise exception    number = int(input("Enter a number: "))    print(f"You entered: {number}")except:    # Handle any exception    print("Invalid input!") # Specific exception typetry:    number = int(input("Enter a number: "))    result = 10 / numberexcept ValueError:    print("Please enter a valid number!")except ZeroDivisionError:    print("Cannot divide by zero!")

Multiple Exceptions

# Handle different exceptions differentlytry:    numbers = [1, 2, 3]    index = int(input("Enter index: "))    print(numbers[index])except ValueError:    print("Please enter a valid integer!")except IndexError:    print("Index out of range!") # Handle multiple exceptions the same waytry:    value = int(input("Enter number: "))    result = 10 / valueexcept (ValueError, ZeroDivisionError):    print("Invalid input or division by zero!") # Multiple except blockstry:    file = open('data.txt', 'r')    content = file.read()    number = int(content)    result = 100 / numberexcept FileNotFoundError:    print("File not found!")except ValueError:    print("File content is not a number!")except ZeroDivisionError:    print("Number cannot be zero!")

Catching Exception Object

# Get exception detailstry:    number = int("abc")except ValueError as e:    print(f"Error occurred: {e}")    print(f"Error type: {type(e)}") # Output:# Error occurred: invalid literal for int() with base 10: 'abc'# Error type: <class 'ValueError'> # Use exception infotry:    result = 10 / 0except ZeroDivisionError as e:    print(f"Math error: {e}")    # Log error, send notification, etc. # Generic exception handlertry:    # Some risky code    value = risky_operation()except Exception as e:    print(f"Something went wrong: {e}")    print(f"Exception type: {type(e).__name__}")

else Clause

else chạy nếu không có exception trong try block.

# else clausetry:    number = int(input("Enter a number: "))except ValueError:    print("Invalid number!")else:    # Only runs if no exception    print(f"Success! You entered: {number}")    result = number * 2    print(f"Double: {result}") # Real-world exampledef read_file(filename):    try:        file = open(filename, 'r')    except FileNotFoundError:        print(f"File not found: {filename}")        return None    else:        # File opened successfully        content = file.read()        file.close()        return content content = read_file('data.txt')if content:    print(content)

finally Clause

finally luôn chạy, dù có exception hay không.

# finally always executestry:    file = open('data.txt', 'r')    content = file.read()    print(content)except FileNotFoundError:    print("File not found!")finally:    print("This always executes") # Use finally for cleanupdef read_file(filename):    file = None    try:        file = open(filename, 'r')        content = file.read()        return content    except FileNotFoundError:        print(f"File not found: {filename}")        return None    finally:        # Cleanup - always close file        if file:            file.close()            print("File closed") # Complete structuretry:    # Try some code    number = int(input("Enter number: "))    result = 10 / numberexcept ValueError:    # Handle specific exception    print("Invalid number!")except ZeroDivisionError:    # Handle another exception    print("Cannot divide by zero!")else:    # Runs if no exception    print(f"Result: {result}")finally:    # Always runs    print("Calculation complete")

Raising Exceptions

Dùng raise để tạo exceptions.

Basic raise

# Raise exception manuallydef check_age(age):    if age < 0:        raise ValueError("Age cannot be negative!")    if age < 18:        raise ValueError("Must be 18 or older!")    return True try:    check_age(-5)except ValueError as e:    print(f"Error: {e}") # Raise without messagedef validate(value):    if not value:        raise ValueError    return True # Re-raise exceptiontry:    number = int("abc")except ValueError:    print("Logging error...")    raise  # Re-raise the same exception

raise with Different Exception Types

# Raise different exceptionsdef divide(a, b):    if not isinstance(a, (int, float)):        raise TypeError("First argument must be a number")    if not isinstance(b, (int, float)):        raise TypeError("Second argument must be a number")    if b == 0:        raise ZeroDivisionError("Cannot divide by zero")    return a / b try:    result = divide(10, "2")except TypeError as e:    print(f"Type error: {e}")except ZeroDivisionError as e:    print(f"Math error: {e}") # Raise with formatted messagedef withdraw(balance, amount):    if amount > balance:        raise ValueError(            f"Insufficient funds: "            f"Balance ${balance}, requested ${amount}"        )    return balance - amount

Built-in Exception Types

Common Exceptions

# ValueError - invalid valueint("abc")  # ValueError # TypeError - wrong type"hello" + 5  # TypeError # ZeroDivisionError - division by zero10 / 0  # ZeroDivisionError # IndexError - invalid indexlist = [1, 2, 3]list[10]  # IndexError # KeyError - invalid keydict = {"name": "Alice"}dict["age"]  # KeyError # AttributeError - invalid attribute"hello".invalid_method()  # AttributeError # FileNotFoundError - file doesn't existopen('nonexistent.txt')  # FileNotFoundError # ImportError - cannot import moduleimport nonexistent_module  # ImportError # NameError - undefined variableprint(undefined)  # NameError

Exception Hierarchy

# All exceptions inherit from BaseExceptionBaseException├── Exception│   ├── ValueError│   ├── TypeError│   ├── ZeroDivisionError│   ├── IndexError│   ├── KeyError│   ├── AttributeError│   ├── FileNotFoundError│   └── ...├── KeyboardInterrupt└── SystemExit # Catch all exceptions (not recommended)try:    # risky code    passexcept Exception as e:    # Catches most exceptions    print(f"Error: {e}") # Be specific (recommended)try:    # risky code    passexcept ValueError:    print("Value error!")except TypeError:    print("Type error!")

Ví Dụ Thực Tế

1. Safe Input Function

def get_integer(prompt, min_value=None, max_value=None):    """    Get integer input with validation.        Args:        prompt (str): Input prompt        min_value (int): Minimum allowed value        max_value (int): Maximum allowed value        Returns:        int: Valid integer    """    while True:        try:            value = int(input(prompt))                        # Check range            if min_value is not None and value < min_value:                print(f"Value must be at least {min_value}")                continue                        if max_value is not None and value > max_value:                print(f"Value must be at most {max_value}")                continue                        return value                except ValueError:            print("Please enter a valid integer!")        except KeyboardInterrupt:            print("\nOperation cancelled")            return None # Usageage = get_integer("Enter your age: ", min_value=0, max_value=150)if age:    print(f"Age: {age}")

2. Safe File Reader

def read_file_safe(filename, encoding='utf-8'):    """    Safely read file with error handling.        Args:        filename (str): File path        encoding (str): File encoding        Returns:        str: File content or None if error    """    try:        with open(filename, 'r', encoding=encoding) as file:            return file.read()        except FileNotFoundError:        print(f"Error: File not found - {filename}")        return None        except PermissionError:        print(f"Error: No permission to read - {filename}")        return None        except UnicodeDecodeError:        print(f"Error: Cannot decode file - {filename}")        print(f"Try different encoding (current: {encoding})")        return None        except Exception as e:        print(f"Unexpected error: {e}")        return None # Usagecontent = read_file_safe('data.txt')if content:    print(content)else:    print("Failed to read file")

3. Calculator with Error Handling

def calculate(a, b, operation):    """    Perform calculation with error handling.        Args:        a (float): First number        b (float): Second number        operation (str): Operation (+, -, *, /)        Returns:        float: Result or None if error    """    try:        # Validate inputs        a = float(a)        b = float(b)                # Perform operation        if operation == '+':            return a + b        elif operation == '-':            return a - b        elif operation == '*':            return a * b        elif operation == '/':            if b == 0:                raise ZeroDivisionError("Cannot divide by zero")            return a / b        else:            raise ValueError(f"Invalid operation: {operation}")        except ValueError as e:        print(f"Value error: {e}")        return None        except ZeroDivisionError as e:        print(f"Math error: {e}")        return None        except Exception as e:        print(f"Unexpected error: {e}")        return None # Usageresult = calculate(10, 5, '+')print(f"Result: {result}")  # 15.0 result = calculate(10, 0, '/')print(f"Result: {result}")  # None (error handled) result = calculate(10, 5, '%')print(f"Result: {result}")  # None (invalid operation)

4. User Registration Validator

def validate_username(username):    """Validate username."""    if len(username) < 3:        raise ValueError("Username must be at least 3 characters")        if len(username) > 20:        raise ValueError("Username must be at most 20 characters")        if not username.isalnum():        raise ValueError("Username must contain only letters and numbers")        return True def validate_email(email):    """Validate email."""    if '@' not in email:        raise ValueError("Email must contain @")        if '.' not in email.split('@')[1]:        raise ValueError("Email must have valid domain")        return True def validate_password(password):    """Validate password."""    if len(password) < 8:        raise ValueError("Password must be at least 8 characters")        if not any(c.isupper() for c in password):        raise ValueError("Password must contain uppercase letter")        if not any(c.islower() for c in password):        raise ValueError("Password must contain lowercase letter")        if not any(c.isdigit() for c in password):        raise ValueError("Password must contain digit")        return True def register_user(username, email, password):    """    Register user with validation.        Returns:        dict: User data or None if error    """    try:        # Validate all fields        validate_username(username)        validate_email(email)        validate_password(password)                # Create user        user = {            'username': username,            'email': email,            'password': password  # Should be hashed in real app!        }                print(f"User {username} registered successfully!")        return user        except ValueError as e:        print(f"Validation error: {e}")        return None # Usageuser = register_user("alice", "[email protected]", "Password123")if user:    print(f"Welcome, {user['username']}!") user = register_user("ab", "invalid", "weak")# Multiple validation errors shown

5. Database Connection Mock

class DatabaseError(Exception):    """Custom database exception."""    pass class ConnectionError(DatabaseError):    """Connection failed."""    pass class QueryError(DatabaseError):    """Query execution failed."""    pass def connect_database(host, port):    """    Connect to database with error handling.        Returns:        bool: True if connected    """    try:        print(f"Connecting to {host}:{port}...")                # Validate parameters        if not host:            raise ValueError("Host cannot be empty")                if not isinstance(port, int):            raise TypeError("Port must be an integer")                if port < 1 or port > 65535:            raise ValueError("Port must be between 1 and 65535")                # Simulate connection        if host == "localhost" and port == 5432:            print("Connected successfully!")            return True        else:            raise ConnectionError(f"Cannot connect to {host}:{port}")        except ValueError as e:        print(f"Invalid parameter: {e}")        return False        except TypeError as e:        print(f"Type error: {e}")        return False        except ConnectionError as e:        print(f"Connection failed: {e}")        return False        except Exception as e:        print(f"Unexpected error: {e}")        return False        finally:        print("Connection attempt completed") def execute_query(query):    """Execute database query."""    try:        if not query:            raise ValueError("Query cannot be empty")                # Check for dangerous operations        dangerous_keywords = ['DROP', 'DELETE', 'TRUNCATE']        if any(keyword in query.upper() for keyword in dangerous_keywords):            raise QueryError("Dangerous operation not allowed")                print(f"Executing: {query}")        print("Query successful!")        return True        except ValueError as e:        print(f"Invalid query: {e}")        return False        except QueryError as e:        print(f"Query error: {e}")        return False # Usageif connect_database("localhost", 5432):    execute_query("SELECT * FROM users")    execute_query("DROP TABLE users")  # Blocked!

Best Practices

# 1. Be specific with exceptions# ❌ Too broadtry:    value = int("abc")except:    print("Error") # ✅ Specifictry:    value = int("abc")except ValueError:    print("Invalid number") # 2. Don't catch everything# ❌ Catches too muchtry:    # code    passexcept Exception:    pass  # Silent failure! # ✅ Let important exceptions propagatetry:    # code    passexcept ValueError:    # Handle expected error    pass# KeyboardInterrupt, SystemExit not caught # 3. Use finally for cleanuptry:    file = open('data.txt', 'r')    # process filefinally:    file.close()  # Always close # 4. Provide helpful error messages# ❌ Not helpfulraise ValueError("Invalid") # ✅ Descriptiveraise ValueError(f"Age must be positive, got {age}") # 5. Don't use exceptions for control flow# ❌ Badtry:    while True:        value = list[index]        index += 1except IndexError:    pass # ✅ Goodfor value in list:    # process value    pass

Common Patterns

# Pattern 1: EAFP (Easier to Ask Forgiveness than Permission)# Pythonic way - try first, handle exceptions # EAFPtry:    value = dictionary[key]except KeyError:    value = default # vs LBYL (Look Before You Leap)if key in dictionary:    value = dictionary[key]else:    value = default # Pattern 2: Retry with limitdef retry_operation(func, max_attempts=3):    """Retry operation with limit."""    for attempt in range(max_attempts):        try:            return func()        except Exception as e:            if attempt == max_attempts - 1:                raise  # Last attempt, re-raise            print(f"Attempt {attempt + 1} failed, retrying...") # Pattern 3: Context-specific exceptionsdef process_data(data):    """Process data with specific exceptions."""    try:        # Validate        if not data:            raise ValueError("Data is empty")                # Process        result = complex_operation(data)                # Return        return result        except ValueError:        # Handle validation errors        return None    except Exception as e:        # Log unexpected errors        log_error(e)        raise

Bài Tập Thực Hành

Bài 1: Safe Division

Viết function safe_divide(a, b):

  • Handle ZeroDivisionError
  • Handle TypeError for non-numeric inputs
  • Return result or None with error message

Bài 2: File Processor

Viết function process_file(filename):

  • Read file
  • Handle FileNotFoundError, PermissionError
  • Count lines, words, characters
  • Use finally to ensure cleanup

Bài 3: Input Validator

Viết class InputValidator:

  • Method get_int() - get integer with retry
  • Method get_float() - get float with retry
  • Method get_choice() - get choice from list
  • Handle all input errors gracefully

Bài 4: Safe JSON Parser

Viết function parse_json_safe(json_string):

  • Parse JSON string
  • Handle JSONDecodeError
  • Return parsed data or None
  • Provide detailed error messages

Bài 5: Retry Decorator

Viết decorator retry(max_attempts=3):

  • Retry function on exception
  • Limit number of attempts
  • Log each attempt
  • Re-raise on final failure

Tóm Tắt

Exception: Error xảy ra khi runtime
try/except: Bắt và xử lý exceptions
else: Chạy nếu không có exception
finally: Luôn chạy (cleanup)
raise: Tạo exception manually
Exception types: ValueError, TypeError, ZeroDivisionError, etc.
Best practice: Be specific, don't catch all, use finally

Bài Tiếp Theo

Bài 14.2: Exception Handling (Phần 2) - Custom exceptions, exception chaining, context managers, và advanced patterns.


Remember:

  • Be specific with exception types!
  • Use finally for cleanup
  • Don't silence exceptions without good reason
  • Provide helpful error messages
  • EAFP is Pythonic!