例外処理したい(try...except...finally / raise

 1try:
 2    例外が発生するかもしれない処理
 3except 例外の種類 as オブジェクト名:
 4    例外が発生したときの処理
 5except KeyboardInterrupt as e:
 6    print("中断しました")
 7    sys.exit(0)  # Ctrl-cを補足して正常終了
 8except Exception as e:
 9    基本的な例外が発生したときの処理
10    print(f"Error: {e}")
11    print(f"{e.args=}")
12    sys.exit(1)  # 想定外のエラーで異常終了
13finally:
14    例外の有無に関係なく実行する処理

try..except..finally構文を使って例外処理(=エラーハンドリング)できます。 except処理は複数回記述でき、finally処理は省略できます。

例外の種類には Pythonにビルトインの例外クラス名、もしくはカスタムした例外クラス名を指定します。 また、発生した例外をオブジェクト名で受け取っておくと、 例外をさらにコントロールできます。 オブジェクト名にはeerrを使うことが多いです。

例外処理は、親クラスの名前でも補足できます。 利用可能なビルトイン例外とその階層構造の詳細は exception hierarchy を参照してください。

ヒント

1def main():
2    try:
3        ...
4    except Exception as e:
5        基本的な例外が発生したときの処理
6    except BaseException as e:
7        KeyboardInteruptなどExceptionで捕捉できない例外が発生したときの処理

例外処理は、基本的にmain関数に実装します。

例外を送出したい(raise

1if 条件確認:
2    条件が正しいときの処理
3else:
4    raise 例外クラス名

raiseを例外を送出(=throw)できます。 例外クラス名は、ビルトインの例外クラスが使えます。 また、カスタムした例外クラスも作成できます。

自作パッケージを作成している場合、 適切な例外を発生させることで、ユーザー(自分自身を含む)に 優しいパッケージになります。

raise or sys.exit

例外(=エラー)が発生するとき、 raiseするか、 sys.exitするかを考える必要があります。 一般的に、 ライブラリ(やサブ関数)の中ではraiseし、 CLI(やメイン関数)で例外をキャッチしてsys.exit すればよさそうです。

インポートして利用するパッケージの場合、 パッケージ内部でsys.exitされていると、 パッケージのユーザーはそこから先、どうすることもできません。 しかし、適切な例外がraiseされていれば、 その例外処理をどうすべきか(キャッチして対処するのか、そのまま握り潰すのか、など) ユーザーが判断できます。

 1def func1():
 2    """サブ関数: 条件にマッチしない場合 ValueErrorを送出
 3    """
 4    if 条件:
 5        条件が正しいときの処理
 6        return
 7    else:
 8        # ここではsys.exitせずにraiseする
 9        msg = "値がおかしいよ"
10        raise ValueError(msg)
11
12def func2():
13    ...
14    if 条件:
15        ...
16    else:
17        msg = "接続失敗"
18        raise ConnectionError(msg)
19
20def main():
21    try:
22        func1()
23        func2()
24    except ValueError as e:
25        print(f"Error: {e}")
26        print(f"{e.args=}")
27        sys.exit()
28    except ConnectionError as e:
29        print(f"Error: {e}")
30        print(f"{e.args=}")
31        sys.exit()
32    except KeyboardInterrupt as e:
33        print(f"Error: {e}")
34        print(f"{e.args=}")
35        sys.exit()
36
37if __name__ == "__main__":
38    main()

このサンプルは、僕がスクリプトやパッケージを作成するときのテンプレ的なものです。 raiseされた例外は親の関数にも伝播します。 インポートした外部パッケージで例外が適切に送出されている場合、 サブ関数の中ではtry...exceptせずにそのままにしておけばOKです。

よく遭遇する例外と確認事項

例外が発生した場合、まずエラーメッセージをきちんと確認することが大切です。 どの例外が、どのファイル(のどのあたり)で発生したかを把握することで、 その理由を考えたり、対応方法を考えたりできます。

以下に遭遇しがちなビルトイン例外と、そのときの確認事項を整理してみました。

ImportError / ModuleNotFoundError:

モジュールのインポートが正しくない場合に発生します。 モジュール名が正しいか、モジュールがインストールされているか、Python実行環境のパスが正しいか、確認しましょう。

AttributeError / KeyError / IndexError / ValueError:

オブジェクト操作が正しくない場合に発生します。 リストの長さをオーバーしていないか、辞書型にないキーを指定していないか、確認しましょう。

NameError:

変数名が正しくないときに発生します。 変数の定義を忘れていないか、変数名にスペルミスがないか、確認しましょう。

SyntaxError / IndentationError:

文法や構文が正しくないときに発生します。 インデントがずれていないか、条件文の末尾の:を忘れていないか、確認しましょう。

FileExistsError / FileNotFoundError:

ファイル操作が正しくないときに発生します。 ファイル名が正しいか、既存のファイルが存在していないか、確認しましょう。

中断処理したい(KeyboardInterrupt

1try:
2    例外が発生するかもしれない処理
3except KeyboardInterrupt as e:
4    中断処理
5    # バッファーの内容をファイルに保存する、など
6    print(f"Error: {e}")
7    sys.exit()

KeyboardInterruptクラスで、中断処理(ctrl-c)したときの例外処理ができます。 中断処理すると、そのときバッファーにあるデータなどは破棄されます。 長時間走らせるようなスクリプトや、CLIツールには必ず中断処理を書いて、 安全に終了できるようにしましょう。

パッケージの例外処理したい

1try:
2    response = requests.get("https://httpbin.org/status/404")
3    response.raise_for_status()
4except requests.exception.RequestException as e:
5    print(f"Error: {e}")

外部パッケージによっては、パッケージ独自の例外クラスがあります。 上記サンプルは、requestsパッケージの例外処理です。 このように、パッケージによっては、カスタムの例外クラスが定義されてます。 詳細はパッケージのドキュメントを確認してください。

例外クラスをカスタムしたい(Exception

 1# MODULE_NAME.py
 2class CustomError(Exception):
 3    """CustomError: 最小構成(何もしない)"""
 4    pass
 5
 6def CustomFunction(...):
 7    if 条件:
 8        正常処理
 9    else:
10        raise CustomError("エラー発生!"):

Exceptionクラスを継承して、例外クラスをカスタムできます。 小規模なスクリプトでは、ビルトインの例外クラスで十分対応できると思うので、 わざわざ例外クラスを自作する必要はありません。

上記のサンプルは、例外クラス名だけを変更しています。 公開されている有名なパッケージでもよく使われている気がします。

 1# exceptions.py
 2class CustomError(Exception):
 3    """CustomError: 自作の例外クラス"""
 4    def __init__(self,
 5                message: str = "デフォルトのエラーメッセージ",
 6                error_code: int | None = None
 7        ):
 8        """CustomErrorを初期化
 9
10        Args:
11            message (str): エラーメッセージ
12            error_code (int): エラーコード
13        """
14        super().__init__(message)    # 親クラスのExceptionクラスの初期化
15        self.message = message
16        self.error_code = error_code
17
18    def __str__(self):
19        """例外の文字列表現
20
21        Returns:
22            str: [エラーコード] エラーメッセージ
23        """
24        if self.error_code is not None:
25            return f"{[{self.error_code}] {self.message}}"
26        return self.message

以下のようなユースケースで、自作を検討したほうがいいかもしれません。

  1. 特定の例外シナリオに対処したい

  2. 例外処理をカプセル化したい

  3. 他の例外と区別したい

中規模〜大規模なパッケージを開発している場合には、複数の例外クラスが必要になるかもしれません。 その場合は、exceptions.pyのようなファイルに整理するのが一般的なようです。

カスタムした例外クラスの作成は@dataclassデコレーターを使って簡略化できます。

 1# exceptions.py
 2from dataclasses import dataclass
 3
 4@dataclass
 5class CustomError(Exception):
 6    """CustomError: 自作の例外クラス
 7
 8    dataclassを使って簡略化
 9    """
10    message: str = "デフォルトのエラーメッセージ"
11    error_code: int | None = None
12
13    def __str__(self):
14        """例外の文字列表現"""
15        if self.error_code is not None:
16            return f"[{self.error_code}] {self.message}"
17        return self.message
 1from .exceptions import CustomError
 2
 3def custom_function(...):
 4    if 条件:
 5        正常処理
 6    else:
 7        raise CustomError("エラー発生!", 101)
 8
 9try:
10    custom_function(...)
11except CustomError as e:
12    print(e)
13    # => "[101] エラー発生!"

リファレンス