Python Exception Handling | try-except, else, finally, raise, Custom Exceptions

Python Exception Handling | try-except, else, finally, raise, Custom Exceptions

이 글의 핵심

Hands-on guide to Python exception handling: try-except chains, else/finally, raise and re-raise, custom exceptions, and practical patterns.

Introduction

“Handling errors gracefully”

Exception handling is central to building stable, maintainable Python programs.


1. Basic exception handling

try-except

# Basic form
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero")
    result = None

# Multiple exception types
try:
    number = int(input("Enter a number: "))
    result = 10 / number
except ValueError:
    print("Please enter a valid number")
except ZeroDivisionError:
    print("Enter a non-zero number")

# Bind the exception object
try:
    file = open('missing.txt', 'r')
except FileNotFoundError as e:
    print(f"Error: {e}")

2. try-except-else-finally

Full structure

try:
    # Code that might fail
    file = open('data.txt', 'r')
    content = file.read()
except FileNotFoundError:
    # Runs when an exception occurs
    print("File not found")
else:
    # Runs only if no exception in try
    print(f"Read OK: {len(content)} characters")
finally:
    # Always runs (cleanup)
    if 'file' in locals():
        file.close()
    print("Done")

3. Raising exceptions (raise)

Basic raise

def divide(a, b):
    if b == 0:
        raise ValueError("b cannot be zero")
    return a / b

try:
    result = divide(10, 0)
except ValueError as e:
    print(f"Error: {e}")

Re-raising

def process_data(data):
    try:
        result = int(data)
    except ValueError:
        print("Conversion failed")
        raise  # propagate to caller

try:
    process_data("abc")
except ValueError:
    print("Handled at outer level")

4. Custom exceptions

User-defined exceptions

class InsufficientBalanceError(Exception):
    """Raised when withdrawal exceeds balance."""
    def __init__(self, balance, amount):
        self.balance = balance
        self.amount = amount
        super().__init__(f"Insufficient balance: {balance} (needed: {amount})")

class BankAccount:
    def __init__(self, owner, balance):
        self.owner = owner
        self.balance = balance
    
    def withdraw(self, amount):
        if amount > self.balance:
            raise InsufficientBalanceError(self.balance, amount)
        self.balance -= amount
        return self.balance

# Usage
account = BankAccount("Alice", 10000)

try:
    account.withdraw(15000)
except InsufficientBalanceError as e:
    print(e)  # Insufficient balance: 10000 (needed: 15000)
    print(f"Current balance: {e.balance}")

5. Common built-in exceptions

Frequently used types

# ValueError: wrong value
try:
    int("abc")
except ValueError:
    print("Conversion failed")

# TypeError: wrong types
try:
    "hello" + 5
except TypeError:
    print("Type mismatch")

# KeyError: missing dict key
try:
    data = {'name': 'Alice'}
    print(data['age'])
except KeyError:
    print("Key missing")

# IndexError: index out of range
try:
    arr = [1, 2, 3]
    print(arr[10])
except IndexError:
    print("Index out of range")

# FileNotFoundError: missing file
try:
    open('missing.txt', 'r')
except FileNotFoundError:
    print("File not found")

6. Practical examples

Safe JSON file reading

import json

def safe_read_json(filename):
    """Read a JSON file safely."""
    try:
        with open(filename, 'r', encoding='utf-8') as f:
            return json.load(f)
    except FileNotFoundError:
        print(f"File not found: {filename}")
        return {}
    except json.JSONDecodeError as e:
        print(f"JSON parse error: {e}")
        return {}
    except Exception as e:
        print(f"Unexpected error: {e}")
        return {}

Retry logic

import time

def retry_operation(func, max_attempts=3):
    """Retry func until success or attempts exhausted."""
    for attempt in range(max_attempts):
        try:
            return func()
        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")
            if attempt < max_attempts - 1:
                time.sleep(1)
            else:
                raise

# Usage
def unstable_operation():
    import random
    if random.random() < 0.7:
        raise ConnectionError("connection failed")
    return "success"

try:
    result = retry_operation(unstable_operation)
    print(result)
except Exception as e:
    print(f"Final failure: {e}")

Practical tips

Exception-handling best practices

# ✅ Catch specific exceptions
try:
    value = int(user_input)
except ValueError:
    print("Please enter a number")

# ❌ Bare except Exception (harder to debug)
try:
    value = int(user_input)
except Exception:
    print("Something went wrong")

# ✅ Use the exception message
try:
    file = open('data.txt', 'r')
except FileNotFoundError as e:
    print(f"File error: {e}")

# ✅ Prefer context managers over manual close in finally
try:
    with open('data.txt', 'r') as file:
        pass
finally:
    pass  # file already closed by with

Summary

Key takeaways

  1. try-except: handle expected failures.
  2. finally: always runs—use for cleanup when not using with.
  3. raise: signal errors; bare raise re-raises the current exception.
  4. Custom exceptions: subclass Exception and store context on self.
  5. Best practice: catch narrow types; avoid swallowing bugs with overly broad handlers.

Next steps