Python try exceptで業務エラー処理を完全マスター|実務パターン解説
Pythonを使った業務開発では、エラーハンドリングが非常に重要です。APIの呼び出し失敗、データベース接続エラー、ファイル読み込み失敗など、本番環境ではあらゆる予期しない状況が発生します。単に「エラーをキャッチする」だけではなく、適切にログを記録し、ユーザーに通知し、必要に応じてリトライするなど、業務レベルのエラーハンドリングが求められます。
この記事では、Pythonの try-except-finally 構文を使った実務的なエラー処理パターンを、具体的なコード例を交えて解説します。
簡易的な解説:try-except-finally の基本
Pythonのエラーハンドリングの基本は try-except-finally です。
- try:エラーが発生する可能性のあるコードを記述
- except:特定の例外を捕捉して処理
- finally:エラーの有無に関わらず必ず実行される処理
- else:エラーが発生しなかった場合だけ実行
基本的な書き方は以下の通りです。
try:
# エラーが発生する可能性のあるコード
result = 10 / 0
except ZeroDivisionError as e:
# 特定の例外をキャッチして処理
print(f\"エラーが発生しました: {e}\")
except Exception as e:
# その他の例外をキャッチ
print(f\"予期しないエラー: {e}\")
finally:
# 必ず実行される処理
print(\"処理完了\")\n
業務でのユースケース
実務開発では、以下のようなシーンで try-except が活躍します。
1. 外部API呼び出しのエラーハンドリング
APIサーバーがダウンしている、タイムアウト、レート制限など、様々なエラーが考えられます。
2. データベース操作のエラーハンドリング
接続エラー、トランザクション失敗、制約違反など、DBMSから返されるエラーに対応する必要があります。
3. ファイルI/Oのエラーハンドリング
ファイルが見つからない、パーミッション不足、ディスク容量不足など、ファイル操作固有のエラーが発生します。
4. データ処理のバリデーション
CSVやJSONの形式が不正、必須項目が欠落しているなど、データの異常を検出する必要があります。
実装コード:実務で使えるパターン集
パターン1:ログ出力を含むAPI呼び出し
業務では、エラーが発生した時点をログに記録することが必須です。以下は、外部APIを呼び出す時の実装例です。
import requests\nimport logging\nfrom typing import Optional, Dict, Any\n\nlogger = logging.getLogger(__name__)\n\ndef fetch_user_data(user_id: str) -> Optional[Dict[str, Any]]:\n \"\"\"外部APIからユーザーデータを取得\"\"\"\n url = f\"https://api.example.com/users/{user_id}\"\n \n try:\n logger.info(f\"ユーザーデータ取得開始: user_id={user_id}\")\n response = requests.get(url, timeout=5)\n response.raise_for_status() # ステータスコード4xx, 5xxでエラー発生\n \n data = response.json()\n logger.info(f\"ユーザーデータ取得成功: user_id={user_id}\")\n return data\n \n except requests.exceptions.Timeout:\n logger.error(f\"タイムアウト: user_id={user_id}\")\n return None\n \n except requests.exceptions.ConnectionError:\n logger.error(f\"接続エラー: APIサーバーに接続できません\")\n return None\n \n except requests.exceptions.HTTPError as e:\n logger.error(f\"HTTPエラー: status_code={e.response.status_code}, user_id={user_id}\")\n return None\n \n except ValueError as e:\n logger.error(f\"JSON解析エラー: {e}\")\n return None\n \n except Exception as e:\n logger.exception(f\"予期しないエラーが発生: {e}\")\n return None\n\n
パターン2:リトライロジック付きのAPI呼び出し
ネットワークエラーやサーバー一時的なダウンに対応するため、リトライは業務では必須パターンです。
import time\nfrom functools import wraps\n\ndef retry_on_exception(max_retries: int = 3, wait_seconds: int = 2):\n \"\"\"指定回数までリトライするデコレータ\"\"\"\n def decorator(func):\n @wraps(func)\n def wrapper(*args, **kwargs):\n last_exception = None\n \n for attempt in range(1, max_retries + 1):\n try:\n logger.info(f\"{func.__name__} 試行 {attempt}/{max_retries}\")\n return func(*args, **kwargs)\n \n except (requests.exceptions.Timeout, \n requests.exceptions.ConnectionError) as e:\n last_exception = e\n \n if attempt < max_retries:\n logger.warning(\n f\"{func.__name__} 失敗(試行{attempt})、\"\n f\"{wait_seconds}秒後にリトライします: {e}\"\n )\n time.sleep(wait_seconds)\n else:\n logger.error(\n f\"{func.__name__} {max_retries}回の試行後も失敗\"\n )\n \n except requests.exceptions.HTTPError as e:\n # HTTPエラー(4xx, 5xx)はリトライしない\n logger.error(f\"HTTPエラー(リトライしない): {e}\")\n raise\n \n raise last_exception\n \n return wrapper\n return decorator\n\n@retry_on_exception(max_retries=3, wait_seconds=2)\ndef fetch_user_data_with_retry(user_id: str) -> Dict[str, Any]:\n \"\"\"リトライ機能付きのユーザーデータ取得\"\"\"\n response = requests.get(\n f\"https://api.example.com/users/{user_id}\",\n timeout=5\n )\n response.raise_for_status()\n return response.json()\n\n
パターン3:データベース操作のトランザクション処理
DBの更新操作では、複数のクエリを一つのトランザクション内で実行し、エラーが発生したら全ロールバックするパターンが一般的です。
import psycopg2\nfrom contextlib import contextmanager\nfrom typing import Generator\n\nclass DatabaseConnection:\n def __init__(self, connection_string: str):\n self.connection_string = connection_string\n self.conn = None\n \n @contextmanager\n def transaction(self) -> Generator:\n \"\"\"トランザクション処理を安全に実行するコンテキストマネージャー\"\"\"\n try:\n self.conn = psycopg2.connect(self.connection_string)\n logger.info(\"データベース接続成功\")\n yield self.conn\n self.conn.commit()\n logger.info(\"トランザクションコミット\")\n \n except psycopg2.DatabaseError as e:\n if self.conn:\n self.conn.rollback()\n logger.error(f\"データベースエラーが発生、ロールバック: {e}\")\n raise\n \n except psycopg2.IntegrityError as e:\n if self.conn:\n self.conn.rollback()\n logger.error(f\"制約違反エラー、ロールバック: {e}\")\n raise\n \n except Exception as e:\n if self.conn:\n self.conn.rollback()\n logger.exception(f\"予期しないエラーが発生、ロールバック: {e}\")\n raise\n \n finally:\n if self.conn:\n self.conn.close()\n logger.info(\"データベース接続クローズ\")\n\n# 使用例\ndb = DatabaseConnection(\"postgresql://user:password@localhost/dbname\")\n\ntry:\n with db.transaction() as conn:\n cursor = conn.cursor()\n # 複数のINSERT/UPDATE操作\n cursor.execute(\n \"INSERT INTO users (name, email) VALUES (%s, %s)\",\n (\"山田太郎\", \"yamada@example.com\")\n )\n cursor.execute(\n \"UPDATE user_stats SET total_users = total_users + 1\"\n )\n logger.info(\"データベース更新完了\")\n \nexcept psycopg2.Error as e:\n logger.error(f\"データベース操作失敗: {e}\")\n # 上位で適切に処理(ユーザーへの通知など)\n\n
パターン4:ファイル操作とバリデーション
CSVファイルを読み込む場合、ファイルが存在しない、形式が不正、必須列が欠落しているなど、複数の層でエラーハンドリングが必要です。
import csv\nfrom pathlib import Path\nfrom dataclasses import dataclass\nfrom typing import List\n\n@dataclass\nclass UserRecord:\n name: str\n email: str\n phone: str\n\ndef load_users_from_csv(file_path: str) -> List[UserRecord]:\n \"\"\"CSVファイルからユーザーデータを読み込む\"\"\"\n users = []\n file_p = Path(file_path)\n \n try:\n # ファイルの存在確認\n if not file_p.exists():\n raise FileNotFoundError(f\"ファイルが見つかりません: {file_path}\")\n \n logger.info(f\"ファイル読み込み開始: {file_path}\")\n \n with open(file_p, 'r', encoding='utf-8') as f:\n reader = csv.DictReader(f)\n \n # ヘッダー検証\n required_columns = {'name', 'email', 'phone'}\n if not required_columns.issubset(set(reader.fieldnames or [])):\n missing = required_columns - set(reader.fieldnames or [])\n raise ValueError(f\"必須列が欠落しています: {missing}\")\n \n # 各行を処理\n for row_num, row in enumerate(reader, start=2): # start=2はヘッダー行をスキップ\n try:\n # データの検証\n if not row.get('name') or not row.get('name').strip():\n raise ValueError(f\"行{row_num}: nameが空です\")\n \n if not row.get('email') or '@' not in row.get('email', ''):\n raise ValueError(f\"行{row_num}: emailの形式が不正です\")\n \n if not row.get('phone') or not row.get('phone').isdigit():\n raise ValueError(f\"行{row_num}: phoneは数字である必要があります\")\n \n user = UserRecord(\n name=row['name'].strip(),\n email=row['email'].strip(),\n phone=row['phone'].strip()\n )\n users.append(user)\n \n except ValueError as e:\n logger.warning(f\"行のスキップ: {e}\")\n # 1行のエラーで全体を失敗させず、スキップして続行\n continue\n \n logger.info(f\"ファイル読み込み完了: {len(users)}件のユーザーを読み込み\")\n return users\n \n except FileNotFoundError as e:\n logger.error(f\"ファイルエラー: {e}\")\n raise\n \n except ValueError as e:\n logger.error(f\"ヘッダーエラー: {e}\")\n raise\n \n except UnicodeDecodeError:\n logger.error(\"ファイルのエンコーディングがUTF-8ではありません\")\n raise\n \n except Exception as e:\n logger.exception(f\"予期しないエラーが発生: {e}\")\n raise\n\n
よくある応用パターン
パターン5:カスタム例外クラス
業務では、エラーの種類を細かく分類し、呼び出し側で適切に処理したいケースが多いです。カスタム例外クラスを定義することで、エラーハンドリングがより明確になります。
class ApplicationError(Exception):\n \"\"\"アプリケーション固有の基本例外\"\"\"\n pass\n\nclass ValidationError(ApplicationError):\n \"\"\"バリデーションエラー\"\"\"\n def __init__(self, field: str, message: str):\n self.field = field\n self.message = message\n super().__init__(f\"Validation Error [{field}]: {message}\")\n\nclass ExternalAPIError(ApplicationError):\n \"\"\"外部API呼び出し時のエラー\"\"\"\n def __init__(self, api_name: str, status_code: int, message: str):\n self.api_name = api_name\n self.status_code = status_code\n super().__init__(f\"API Error [{api_name}] {status_code}: {message}\")\n\nclass DatabaseError(ApplicationError):\n \"\"\"データベース操作時のエラー\"\"\"\n pass\n\ndef validate_email(email: str) -> str:\n \"\"\"メールアドレスのバリデーション\"\"\"\n if not email or '@' not in email:\n raise ValidationError('email', 'メールアドレスの形式が不正です')\n return email\n\ndef create_user(name: str, email: str) -> dict:\n \"\"\"ユーザー作成処理\"\"\"\n try:\n # バリデーション\n if not name or len(name) < 2:\n raise ValidationError('name', '名前は2文字以上である必要があります')\n \n validated_email = validate_email(email)\n \n # DB保存(省略)\n logger.info(f\"ユーザー作成成功: {name}\")\n return {\"id\": 1, \"name\": name, \"email\": validated_email}\n \n except ValidationError as e:\n # バリデーションエラーはログレベルはWARNING\n logger.warning(f\"バリデーション失敗: {e}\")\n raise\n \n except DatabaseError as e:\n logger.error(f\"データベース保存失敗: {e}\")\n raise\n \n except Exception as e:\n logger.exception(f\"予期しないエラー: {e}\")\n raise ApplicationError(f\"ユーザー作成処理失敗: {e}\")\n\n
パターン6:複数の操作を順序実行し、一つのエラーで全体を失敗させる
複数のAPI呼び出しやDB操作を順序実行する場合、どれか一つが失敗したら後続の処理をスキップするパターンです。
def process_order(order_id: str) -> bool:\n \"\"\"注文処理(複数ステップ)\"\"\"\n try:\n # ステップ1: 在庫確認\n logger.info(f\"ステップ1: 在庫確認開始 (order_id={order_id})\")\n if not check_inventory(order_id):\n raise ApplicationError(\"在庫が不足しています\")\n logger.info(\"ステップ1: 在庫確認完了\")\n \n # ステップ2: 決済処理\n logger.info(\"ステップ2: 決済処理開始\")\n payment_result = process_payment(order_id)\n if not payment_result:\n raise ExternalAPIError(\"PaymentAPI\", 500, \"決済処理に失敗しました\")\n logger.info(\"ステップ2: 決済処理完了\")\n \n # ステップ3: 配送手配\n logger.info(\"ステップ3: 配送手配開始\")\n shipping_result = arrange_shipping(order_id)\n if not shipping_result:\n # 決済は完了しているため、ロールバック処理が必要\n try:\n refund_payment(order_id)\n logger.warning(\"決済をキャンセルしました\")\n except Exception as e:\n logger.error(f\"返金処理失敗: {e}\")\n raise\n raise ApplicationError(\"配送手配に失敗しました\")\n logger.info(\"ステップ3: 配送手配完了\")\n \n logger.info(f\"注文処理完了: {order_id}\")\n return True\n \n except ApplicationError as e:\n logger.error(f\"注文処理失敗: {e}\")\n return False\n \n except Exception as e:\n logger.exception(f\"予期しないエラー: {e}\")\n return False\n\n
注意点:業務で避けるべきパターン
1. 単純に例外を無視する
以下のコードは絶対に避けるべきです。
# ❌ 悪い例\ntry:\n fetch_user_data(user_id)\nexcept:\n pass # エラーを完全に無視\n\n\nこれでは、何が起きているのか全く分からず、デバッグが困難になります。2. 汎用的すぎる例外処理
以下のように Exception でキャッチするのは、最後の砦として使用すべきです。
# ⚠️ 避けるべき\ntry:\n response = requests.get(url)\nexcept Exception as e:\n logger.error(f\"エラー: {e}\")\n\n# ✅ 良い例\ntry:\n response = requests.get(url)\nexcept requests.exceptions.Timeout:\n logger.error(\"タイムアウト\")\nexcept requests.exceptions.HTTPError as e:\n logger.error(f\"HTTPエラー: {e.response.status_code}\")\nexcept Exception as e:\n logger.exception(f\"予期しないエラー: {e}\")\n\n3. ログ記録なしでエラーをキャッチ
業務では必ずログを記録してください。
# ❌ 悪い例\ntry:\n result = some_operation()\nexcept Exception as e:\n return None # エラーログなし\n\n# ✅ 良い例\ntry:\n result = some_operation()\nexcept SpecificError as e:\n logger.error(f\"エラーが発生: {e}\")\n return None\n\n4. リソースのクリーンアップを忘れる
ファイルやDB接続は、必ずクローズする必要があります。
# ❌ 悪い例\ntry:\n f = open('file.txt')\n data = f.read()\nexcept Exception as e:\n logger.error(e)\n# ファイルが閉じられない可能性\n\n# ✅ 良い例\ntry:\n with open('file.txt') as f:\n data = f.read()\nexcept Exception as e:\n logger.error(e)\n# with文で自動的にクローズされる\n\nまとめ
Pythonの try-except-finally を使った業務レベルのエラーハンドリングは、単にエラーをキャッチするだけではなく、以下の要素を含む必要があります。
- 適切なログ記録:問題発生時の状況を把握するため、詳細なログを残す
- 例外の種別分け:エラー種別ごとに異なる処理を行う
- リトライロジック:一時的なエラーに対応するため、再試行を実装する
- トランザクション管理:DBやAPI操作の一貫性を保つため、全体の成功・失敗を管理する
- リソースのクリーンアップ:ファイルやDB接続を確実にクローズする
- カスタム例外:呼び出し側で適切に処理できるよう、エラー情報を構造化する
本番環境では予期しない状況が必ず発生します。実務で堅牢なシステムを構築するために、これらのパターンを参考に、適切なエラーハンドリングを実装してください。

