実務で使えるPythonエラー処理パターン7選 ── 「落ちないコード」の書き方

Pythonは書きやすい言語ですが、エラー処理を適当にすると本番で静かに機能停止するタイプの事故が起きます。スタックトレースもアラートも出ない。ただ3日前から動いていなかった、という状況です。

この記事では、初心者〜中級者がそのまま使える実務頻出のエラー処理パターン7つを紹介します。すべてPython 3.6+の標準ライブラリだけで動きます。

始める前にひとつ。例外処理は「あとから付ける保険」ではなく、設計の一部です。エラーを起きないようにするのではなく、起きても壊れないようにする ── それが本当のエラー設計です。

1. 基本のtry / except ── まずはここから

Pythonのエラー処理の土台です。例外を種類ごとに分けて、それぞれ適切に対応します。

Python
def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "0では割れません"
    except TypeError:
        return "数値を入力してください"

print(divide(10, 2))    # 5.0
print(divide(10, 0))    # 0では割れません
print(divide(10, "a"))  # 数値を入力してください

鉄則:裸のexcept:は絶対に書かない。必ず例外の種類を指定してください。裸のexceptはKeyboardInterruptSystemExitまで捕まえてしまい、デバッグが不可能になります。

初心者あるある:

Python
# これは絶対ダメ
except Exception:
    pass

全エラーを握り潰すこのパターン。3週間後に何かが壊れたとき、手がかりはゼロです。最低限、ログに出しましょう:

Python
except ZeroDivisionError as e:
    logging.error("除算失敗: %s", e)

まとめ:例外は種類別にキャッチ。except: passは時限爆弾だと思ってください。

2. finally ── 必ず後処理する

ファイル、DB接続、ソケット ── 開いたら必ず閉じる。成功しても失敗しても。それがfinallyの役割です。

Python
f = None
try:
    f = open("data.txt", "w")
    f.write("hello")
except IOError as e:
    print("書き込み失敗:", e)
finally:
    if f:
        f.close()
    print("クリーンアップ完了")

finally何があっても実行されます。例外が発生しても、tryの中でreturnしても。

よくあるミス:open()自体が失敗した場合、fは未定義です。finally内のf.close()NameErrorが発生し、エラーが二重になります。必ずf = Noneで初期化しておきましょう。

実務での恐怖体験:バッチ処理でファイルハンドルを閉じ忘れ → ファイルロックが残る → 翌朝のスケジュール実行が全滅。地味に、でも確実に起きます。

まとめ:後処理が必要ならfinally。ただしリソースが生成されていない場合のガードも忘れずに。

3. with文 ── 実務はこれが主流

実際の現場でfinally: f.close()を手書きすることはほとんどありません。with文が自動的にやってくれます。

Python
try:
    with open("data.txt", "w") as f:
        f.write("hello")
except IOError as e:
    print("書き込み失敗:", e)

with文はブロックを抜けるとき、例外が起きても確実にファイルを閉じます。

複数ファイルを同時に開くこともできます:

Python
with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read())

コードレビューでopen()withなしで使っていると、ほぼ確実に指摘されます。それくらい標準的な書き方です。

まとめ:ファイル処理 = with。例外なし(ダジャレではなく)。

4. raiseで例外を再送出する

ログは出したいが、エラー自体は上位に伝えたい。そんなときはraiseで再送出します。

Python
import logging

def process(data):
    try:
        return transform(data)
    except Exception as e:
        logging.error("処理失敗: %s", e)
        raise

raiseを書かないと、関数はNoneを返し、呼び出し元は成功したと思い込みます。「バッチは成功したのにデータがおかしい」── 実務で最もよくある事故の原因のひとつです。

元のエラーにコンテキストを追加したい場合は、例外チェーンを使います:

Python
class ProcessingError(Exception):
    pass

try:
    result = transform(data)
except ValueError as e:
    raise ProcessingError("入力データ不正") from e

from eで元のトレースバックが保持されるため、デバッグ時に完全なエラーチェーンを確認できます。

まとめ:ログだけで終わらせない。処理できないエラーはraiseで上に投げる。

5. カスタム例外 ── 意味のあるエラー型を作る

組み込みの例外は汎用的すぎます。ValueErrorだけでは、何が不正なのかコードから読み取れません。カスタム例外を作ると、コードが自己文書化されます。

Python
class ValidationError(Exception):
    """入力バリデーション失敗時の例外。"""
    pass

class APIError(Exception):
    """外部API呼び出し失敗時の例外。"""
    def __init__(self, status_code, message):
        self.status_code = status_code
        super().__init__(f"{status_code}: {message}")

def validate_age(age):
    if age < 0:
        raise ValidationError("年齢が不正です")
    return age

try:
    validate_age(-5)
except ValidationError as e:
    print(e)  # 年齢が不正です

ルール:

• 必ずExceptionを継承する(BaseExceptionはNG)。
• 名前に意味を持たせる:PaymentDeclinedError > Error1
• 小さいプロジェクトで何十個も作らない ── ノイズになるだけです。

まとめ:カスタム例外は設計ツール。エラーハンドリングがドキュメントのように読めるようになります。

6. assert ── 開発時の簡易チェック

assertは開発中に前提条件を検証するための機能です。条件がFalseならAssertionErrorが発生します。

Python
def withdraw(balance, amount):
    assert amount > 0, "金額は正の値"
    assert balance >= amount, "残高不足"
    return balance - amount

print(withdraw(100, 50))  # 50
print(withdraw(100, 200)) # AssertionError

重要:assertは無効化できますpython -O(最適化モード)で実行すると、すべてのassert文が削除されます。つまり、ビジネスロジックや入力バリデーションに使ってはいけません。

本番のバリデーションには明示的なチェックを:

Python
def withdraw(balance, amount):
    if amount <= 0:
        raise ValueError("金額は正の値にしてください")
    if balance < amount:
        raise ValueError("残高不足です")
    return balance - amount

まとめ:assertは開発ツール。本番のガードには使わない。

7. リトライ処理 ── 通信は失敗するもの

APIはタイムアウトする。DB接続は切れる。DNSは引けないことがある。外部サービスと通信するコードにおいて、リトライは必須です。

Python
import time
import random

def call_api():
    if random.random() < 0.7:
        raise ConnectionError("サーバー応答なし")
    return {"status": "ok"}

max_retries = 5
for attempt in range(max_retries):
    try:
        result = call_api()
        print("成功:", result)
        break
    except ConnectionError as e:
        wait = 2 ** attempt  # 指数バックオフ
        print(f"試行{attempt + 1}回目失敗、{wait}秒後にリトライ...")
        time.sleep(wait)
else:
    print("全リトライ失敗")

本番のリトライで守るべきルール:

最大リトライ回数を必ず設定する。無限リトライ = 無限ループ。
指数バックオフ(1秒, 2秒, 4秒, 8秒…)でサーバーを叩きすぎない。
一時的なエラーだけリトライ。400 Bad Requestをリトライしても意味はない。
for/else構文はリトライに最適。elseはbreakされなかった場合に実行される。

breakの書き忘れがリトライコードの無限ループ原因No.1です。必ず確認しましょう。

まとめ:ネットワーク通信にはリトライ必須。例外なし。

まとめ:エラー処理は「設計」である

7つのパターンの全体像:

1. try/except ── 種類別にキャッチ、裸のexceptは禁止。
2. finally ── リソースの確実なクリーンアップ。
3. with ── Pythonらしいリソース管理。
4. raise ── ログだけで終わらせない、再送出。
5. カスタム例外 ── 自己文書化されたエラー型。
6. assert ── 開発時のみの前提チェック。
7. リトライ ── ネットワーク通信の必須パターン。

深夜3時に叩き起こされるのは、巧妙なコードを書いた人ではなく、エラーケースの処理を忘れた人です。例外処理は、エラーを防ぐことではなく、エラーが起きてもシステムが壊れないようにする設計です。

どんなコードベースでも、失敗を処理するコードは、成功を処理するコードと同じくらい重要です。現場で一番評価されるのは「落ちないコード」を書く人です。

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です