閉じる

Pythonで独自例外を定義する方法まとめ(Exception継承)

Pythonでは標準の例外クラスだけでなく、自分のアプリやライブラリに適した独自の例外を定義できます。

独自例外を使うと、原因の特定やハンドリングの粒度が上がり、コードの見通しが大きく改善します。

本記事では、Exceptionを継承して独自例外を定義する実践方法を、初心者にも分かりやすいサンプルとともに丁寧に解説します。

独自例外とは?Pythonの基本

例外(エラー)の役割とメリット

Pythonにおける例外は、通常の処理では対処できない事態を通知する仕組みです。

例外が発生すると、その場の処理は中断され、対応するexcept節に制御が移ります。

もし捕捉されなければ、スタックトレース(Traceback)が表示されプログラムは終了します。

例外を使うメリットは、異常系を明確に切り出し、通常のロジックと分離できることです。

例外により、コードは読みやすく、保守しやすくなります

また、例外の種類に応じて処理を分岐できるため、原因に合った対処を行いやすくなります。

独自例外を定義する理由

標準のValueErrorKeyErrorなどは便利ですが、アプリ固有のドメインには曖昧なことがあります。

例えば「注文の状態が不正」と「APIのレート制限に達した」は、どちらもRuntimeErrorで表現できますが、同じ型だと区別しづらくなります。

そこで独自例外を用意して、意味のある名前で原因を明確化します。

標準例外では足りない場面

  • 設定ファイルの必須キーが欠落している
  • 外部APIの認証に失敗した
  • ビジネスルール違反(例: 注文の状態遷移が無効)
  • 一時的なリトライ可能エラーと恒久的なエラーを区別したい

こうした場面では、独自の例外型を導入すると、ハンドリングが簡潔になり、上位コードでの判定やログ出力も明快になります。

Pythonで独自例外を定義する(Exception継承)

最小の例(class MyError(Exception))

独自例外はExceptionを継承してシンプルに定義できます。

Python
# 最小の独自例外の定義と、未捕捉時の挙動を確認する例

class MyError(Exception):
    """アプリ固有のエラーを表す最小の例外クラス"""
    pass

def run():
    # 何らかのエラー条件を検出したと仮定して、意図的に発生させる
    raise MyError("何かがうまくいきませんでした")

run()  # try-exceptで捕まえないので、Tracebackが表示される

出力例(Traceback):

Traceback (most recent call last):
  File "example.py", line 12, in <module>
    run()
  File "example.py", line 9, in run
    raise MyError("何かがうまくいきませんでした")
MyError: 何かがうまくいきませんでした

上記のように未捕捉だとプログラムは停止します。

実際のアプリでは、上位で適切に捕捉してログ出力や復旧処理を行います。

例外メッセージを持たせる(__init__)

メッセージや原因となった値など、追加情報を属性として保持すると、デバッグやログが楽になります。

Python
# メッセージと関連情報を属性で持たせる例
# 文字列だけでなく、原因のキーやIDなどを属性で渡すと、機械可読なハンドリングが可能

class ConfigKeyMissingError(Exception):
    """設定ファイルの必須キーが欠落していることを表す例外"""
    def __init__(self, key: str, *, filepath: str | None = None):
        self.key = key
        self.filepath = filepath
        # 親クラスにメッセージを渡す。str(e) 時はこのメッセージが使われる
        msg = f"必須キーが見つかりません: key={key}"
        if filepath:
            msg += f" (file={filepath})"
        super().__init__(msg)

def load_config():
    # ダミー: 必須キー "token" がないと仮定
    raise ConfigKeyMissingError("token", filepath="settings.toml")

try:
    load_config()
except ConfigKeyMissingError as e:
    # 例外オブジェクトから属性を取り出して用途別に処理できる
    print("短いメッセージ:", str(e))
    print("欠落キー:", e.key)
    print("ファイル:", e.filepath)
実行結果
短いメッセージ: 必須キーが見つかりません: key=token (file=settings.toml)
欠落キー: token
ファイル: settings.toml

メッセージは人間向け、属性はプログラム向けという分担にすると、ログと分岐の両方で使いやすくなります。

パッケージ用の基底例外(BaseError)を用意

ライブラリや大きなアプリでは、自分の名前空間用の基底例外を1つ用意すると、その配下の全エラーを一括捕捉できます。

Python
# ライブラリ "acme" を想定した基底例外とサブクラス群の例

class AcmeError(Exception):
    """acmeライブラリ全般の基底例外"""
    pass

class AcmeConfigError(AcmeError):
    """設定関連のエラー"""
    pass

class AcmeAPIError(AcmeError):
    """外部API呼び出し関連のエラー"""
    def __init__(self, status: int, message: str = "API error"):
        self.status = status
        super().__init__(f"{message} (status={status})")

def call_api():
    # ダミー: 503エラーが返ってきた
    raise AcmeAPIError(503, "Service Unavailable")

try:
    call_api()
except AcmeAPIError as e:
    print("APIだけ個別にリトライ:", e)
except AcmeError as e:
    # 他のacmeエラーはまとめて処理
    print("acmeの一般的なエラー:", e)
実行結果
APIだけ個別にリトライ: Service Unavailable (status=503)

このように、まず個別の例外を狭く捕まえ、最後に基底例外AcmeErrorで広く受けると、段階的なハンドリングが可能になります。

命名規則と配置場所(例外クラスの置き場)

独自例外の命名と配置は、読み手の理解に直結します。

  • 命名は「意味のある名詞+Error」を基本にし、一貫性を保ちます。
  • ライブラリや大規模アプリではexceptions.pyなど専用モジュールを用意します。
  • パッケージの__init__.pyで公開したい例外だけ再エクスポートすると使いやすくなります。

良い例/悪い例の比較:

目的悪い例良い例
設定キー欠落MissingConfigKeyMissingError
認証失敗FailedAuthenticationError
無効な状態BadStateInvalidStateError
ドメイン固有RuntimeErrorOrderStatusTransitionError

プロジェクト構成の一例:

acme/
  __init__.py        # from .exceptions import AcmeError, AcmeAPIError, AcmeConfigError
  exceptions.py      # クラス定義を集約
  api.py
  config.py

この構成により、利用側はfrom acme import AcmeErrorのようにシンプルに参照できます。

使い方:raiseとtry-exceptで扱う

raiseで独自例外を発生させる

raiseで任意の場所から意図的に例外を送出します。

既存の例外をラップして原因を保つ場合はraise ... from ...を使います。

Python
# 例外の送出と、原因例外のチェイニング(raise from)の例

class ConfigParseError(Exception):
    pass

def parse_token(d: dict) -> str:
    try:
        # dictからtokenを取り出すが、キーがなければKeyErrorが起きる
        return d["token"]
    except KeyError as e:
        # 原因(KeyError)を保ったまま、より意味のある例外に変換
        raise ConfigParseError("設定からtokenを読み取れませんでした") from e

# デモ
try:
    parse_token({"user": "alice"})  # "token" がない
except ConfigParseError as e:
    print("アプリ向けのわかりやすい例外:", e)
実行結果
アプリ向けのわかりやすい例外: 設定からtokenを読み取れませんでした

raise fromを使うと元の原因がTracebackに残るため、根本原因の特定が容易になります。

try-exceptで特定の例外だけ捕捉

特定の例外だけを捕捉し、それ以外は上位に送ります。

exceptの並び順は狭い型から広い型へ並べます。

Python
class PaymentError(Exception):
    pass

class InsufficientBalanceError(PaymentError):
    pass

def charge(balance: int, amount: int):
    if amount > balance:
        raise InsufficientBalanceError("残高不足です")
    print("決済に成功しました")

try:
    charge(balance=100, amount=120)
except InsufficientBalanceError as e:
    print("残高を確認して再実行してください:", e)
except PaymentError as e:
    print("支払いに関する一般的なエラー:", e)
実行結果
残高を確認して再実行してください: 残高不足です

必要に応じてelsefinallyで、成功時だけの処理や必ず行う後始末を追加できます。

継承関係で広く/狭く捕まえる

継承を活かすと、粒度の違うハンドリングを簡潔に書けます。

順序を誤ると、狭い例外に到達しない点に注意します。

Python
class AppError(Exception):  # 広い
    pass

class DataError(AppError):  # 狭い
    pass

class NetworkError(AppError):  # 狭い
    pass

def do_task(kind: str):
    if kind == "data":
        raise DataError("データ形式が不正")
    elif kind == "net":
        raise NetworkError("タイムアウト")
    else:
        print("正常終了")

for k in ("data", "net", "ok"):
    try:
        do_task(k)
    except DataError as e:           # まず狭い型
        print("[DATA] 再フォーマットを試行:", e)
    except NetworkError as e:        # 次に別の狭い型
        print("[NET] リトライ待機:", e)
    except AppError as e:            # 最後に広い型
        print("[APP] 一般処理:", e)
実行結果
[DATA] 再フォーマットを試行: データ形式が不正
[NET] リトライ待機: タイムアウト
正常終了

ベストプラクティスと注意点

Exceptionを継承する(RuntimeErrorなどは避ける)

独自例外はExceptionを直接継承するのが基本です。

BaseExceptionの直接継承は絶対に避けてください(SystemExitやKeyboardInterruptまで捕まる設計は危険です)。

また、特別な理由がない限り、RuntimeErrorやValueErrorを安易に継承しない方が混乱しません。

意味づけを自前の型名で明確にしましょう。

悪い例:

Python
# 意味があいまいで、外部から見るとRuntimeErrorと区別がつかない
class MyRuntimeError(RuntimeError):
    pass

良い例:

Python
class SpreadsheetImportError(Exception):
    """スプレッドシートの取り込みに失敗したことを表す"""
    pass

メッセージは短く具体的に

メッセージは何が悪いのかを一言で特定できるようにし、詳細は属性へ。

ログに埋もれない短さを心がけます。

悪い良い
うまくいきませんでした顧客IDが無効です: id=abc
エラーが発生しましたCSVの列数が不足しています: expected=5, got=3
保存できませんS3への書き込みに失敗しました(bucket=logs, key=2024-10.csv)

属性で補助情報を渡す例:

Python
class CustomerNotFoundError(Exception):
    def __init__(self, customer_id: str):
        self.customer_id = customer_id
        super().__init__(f"顧客が見つかりません: id={customer_id}")

制御フローに乱用しない

例外は例外的な状況の通知に使い、通常フローの分岐には使いません。

例えば、キーの存在確認はinで済むなら例外で分岐しない方が単純です。

ただしPython文化のEAFP(まず試して、ダメなら例外)が有効な場面もあります。

意図的に使い分けましょう。

Python
# 良くない: 通常フローで例外を多用
def get_price_bad(d: dict) -> int:
    try:
        return d["price"]
    except KeyError:
        return 0  # 本当に0で良いのか判別しづらい

# 良い: 通常は明示的に分岐、足りない時だけ例外
def get_price_good(d: dict) -> int:
    if "price" in d:
        return d["price"]
    raise KeyError("priceが存在しません")

パフォーマンス上、例外はコストが高い点にも注意します。

ホットパスでは通常分岐を優先しましょう。

公開APIは投げる例外を明記する

ライブラリや公共の関数は、発生しうる例外をドキュメント化します。

利用者はexceptの書き方を事前に決められます。

Python
class ReportGenerationError(Exception):
    pass

def generate_report(path: str) -> str:
    """
    指定パスからレポートを生成して、出力ファイルのパスを返します。

    Raises:
        FileNotFoundError: 入力ファイルが見つからない場合
        PermissionError: 出力先に書き込めない場合
        ReportGenerationError: レポート生成の内部エラー一般
    """
    try:
        # ダミー: 何かの内部処理
        raise ValueError("内部の整合性エラー")
    except ValueError as e:
        # 利用者にとって意味のある例外に変換して公開
        raise ReportGenerationError("レポート生成に失敗しました") from e

利用側は次のように書けます。

Python
try:
    generate_report("input.csv")
except FileNotFoundError:
    print("入力ファイルを確認してください")
except ReportGenerationError as e:
    print("生成処理の失敗:", e)
実行結果
生成処理の失敗: レポート生成に失敗しました

まとめ

独自例外は、原因を表す名前必要最小限で具体的なメッセージ、そして属性による機械可読な情報という3点を押さえると、実装も運用もスムーズになります。

まずはExceptionから継承した最小のクラスを定義し、プロジェクト単位で基底例外を用意しましょう。

その上で、狭い型から広い型への順にexceptを書く、raise fromで原因を残す、公開APIで発生しうる例外を明記するといった原則を徹底すれば、エラー処理は格段に読みやすく強靭になります。

独自例外は、Pythonのエラー処理をあなたのドメインに最適化する最良の道具です。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!