基礎

Pythonの例外:try、except、else、finally、raise、独自の例外

更新日: · Skillaria.Top

Pythonの例外は、エラーを表すオブジェクトで、try/except で捕まえるまでプログラムの通常の流れを止めます。else は例外が起きなかったときに、finally は必ず実行されます。自分で例外を起こすには raise を使います。例:raise ValueError("金額が負の値です")。独自の例外は Exception を継承したクラスとして作ります。

Pythonの例外は、エラーが起きたことを知らせるオブジェクトで、プログラムの通常の流れを中断させます。誰も捕まえなければ、Pythonはtracebackを表示して終了します。例外を捕まえるには try/except を使い、自分で発生させるには raise を使います。ここでは、その仕組み、よくあるエラーの種類、そしてエラー処理がエラーの隠蔽にならないためのコツを見ていきます。

Pythonの例外とは

ゼロ除算、存在しないキーへのアクセス、文字列を数値に変換する処理など、操作が成功しないとき、Pythonは例外オブジェクトを作って「送出」します。現在の関数の実行は止まり、例外は呼び出しの連鎖をさかのぼり、適切なハンドラが見つかるまで進みます。どこでも捕まえられなければ、プログラムはクラッシュします。

>>> 10 / 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

よくある質問への答えを書いておきます。Pythonでゼロで割ると、ZeroDivisionError という例外が発生します。これは /、//、% のいずれでも同じです。

tracebackの読み方

tracebackとは、プログラムがエラーに至るまでの経路を示すレポートです。読むときは下から上へ進みます。最後の行が最も重要で、例外の型とメッセージが書かれています。その上には、エラーが起きたコードの行、さらにその上には、その行を呼び出した場所が続き、エントリポイントまでさかのぼります。

Traceback (most recent call last):
  File "tracker.py", line 9, in <module>
    total = parse_amount(row)
  File "tracker.py", line 4, in parse_amount
    return float(raw.strip())
           ~~~~~^^^^^^^^^^^^^
ValueError: could not convert string to float: '百'

Python 3.11以降では、コードの行の下に ~~~^^^ という印が表示されます(PEP 657)。どの式で失敗したのかを示してくれるので、1行に関数呼び出しが続いている場合にとても役立ちます。

「Did you mean」のヒント

Python 3.10以降では、名前を打ち間違えると、近い候補を提案してくれます。Python 3.15では、そのヒントがさらに賢くなりました。他の言語での習慣も認識します。list.push と書くと append を、"coffee".toUpperCase() と書くと upper を提案します。ヒントは入れ子のフィールドにも及びます。辞書が totals という属性に入っているオブジェクトのメソッドを items() と直接呼んでしまった場合、.totals.items を提案します。近い名前の候補は、delattr() でも表示されるようになりました。

Pythonの例外の種類

組み込みの例外は数十種類ありますが、入門の段階で頻繁に目にするのは次のものです。

例外発生する場面例
NameError名前が定義されていない(多くは打ち間違い)print(totl)
TypeErrorその型では操作できない"5" + 3
ValueError型は正しいが、値が不正int("百")
KeyError辞書に存在しないキーがあるd["missing"]
IndexErrorインデックスがリストの範囲外[1, 2][5]
AttributeErrorオブジェクトにそのメソッドや属性がない[].push(1)
ZeroDivisionErrorゼロ除算1 / 0
FileNotFoundErrorファイルが見つからないopen("nope.txt")

SyntaxError(とその特殊な例 IndentationError)は別物です。コードを解析する段階、実行前に発生するため、同じファイルの try では捕まえられません。

例外の階層

例外はクラスであり、階層構造を持っています。頂点は BaseException です。そこから Exception(ふつうのエラーのほとんど)と、KeyboardInterrupt、SystemExit、GeneratorExit などのシステム用の例外が派生します。

BaseException
 ├── KeyboardInterrupt
 ├── SystemExit
 └── Exception
      ├── ArithmeticError
      │    └── ZeroDivisionError
      ├── LookupError
      │    ├── KeyError
      │    └── IndexError
      ├── OSError
      │    └── FileNotFoundError
      ├── TypeError
      └── ValueError

階層は、捕まえるときに重要です。except LookupError は KeyError と IndexError の両方を捕まえます。また、KeyboardInterrupt はコードのエラーではありません。ユーザーが Ctrl+C を押したことをPythonが知らせる仕組みです。except Exception でプログラムの中断を妨げないように、Exception を継承していません。

例外処理:try/except/else/finally

失敗するかもしれないコードは、try ブロックに入れます。except ブロックでは、捕まえる例外の型と、それに対する処理を書きます。Pythonで例外を処理するのは、まさにこの except です。

def parse_amount(raw):
    try:
        value = float(raw)
    except ValueError as e:
        print(f"行をスキップします: {e}")
        return None
    else:
        return value          # 例外が起きなかった場合のみ実行
    finally:
        print("解析完了")      # 必ず実行

print(parse_amount("250"))

この出力は、まず「解析完了」、次に 250.0 の順です。else の中の return は値を覚えておき、Pythonは必ず finally を実行してから、結果を返します。そのため finally は、ファイルを閉じる、接続を解放するといった後片付けに信頼して使えます。

finally の中に独自の return を書くと、前の return を上書きし、例外まで飲み込んでしまいます。Python 3.14以降では、このようなコードに SyntaxWarning が出ます(PEP 765)。finally に return は書かないようにしましょう。

複数の例外を同時に扱う

except は、型ごとに複数書けます。また、型をタプルにまとめることもできます。

try:
    amount = int(data["amount"])
except KeyError:
    amount = 0
except (ValueError, TypeError) as e:
    print("金額の形式が不正です:", e)
    amount = 0

ブロックは上から順に調べられ、最初に一致したものだけが実行されます。そのため、より具体的な型を上に、一般的な型を下に書きます。Python 3.14(PEP 758)からは、型を括弧なしで並べられます(except ValueError, TypeError:)。ただし as を使う場合は、今まで通り括弧が必要です。

エラーのテキストと型を調べる

as e で例外オブジェクトを受け取れます。str(e) はメッセージを、type(e).__name__ は型名を返します。

try:
    int("百")
except ValueError as e:
    print(type(e).__name__)   # ValueError
    print(e)                  # invalid literal for int() with base 10: '百'

tracebackモジュール:完全なエラーテキスト

例外は捕まえたいけれど、トレースバック全体はログやレポートに残したい、という場合があります。その目的には traceback モジュールを使います。

import traceback

try:
    1 / 0
except ZeroDivisionError:
    traceback.print_exc()             # トレースバックを標準エラー出力に表示
    text = traceback.format_exc()     # 同じ内容を文字列として取得

logging モジュールでログを書いているなら、except の中で logging.exception("計算に失敗しました") を呼ぶのが簡単です。トレースバックは自動的にメッセージに追加されます。

Pythonのraise:例外を発生させる

raise 文は、例外を手動で送出します。関数は、そのデータでは作業を実行できないことを、この方法で伝えます。

def add_expense(amount):
    if amount < 0:
        raise ValueError(f"金額は負の値にできません: {amount}")
    ...

add_expense(-5)
# ValueError: 金額は負の値にできません: -5

Pythonの raise は関数ではなく文なので、raise の後に括弧は付けません。括弧の中に渡すのは、例外クラスのコンストラクタの引数です。型は意味に応じて選びます。値が不正なら ValueError、型が不正なら TypeError です。

引数なしのraiseとraise from

except の中で引数なしの raise を書くと、現在の例外が何も変えられずにそのまま上位へ渡されます。ログだけ書いて、処理は呼び出し側に任せたい場合に使います。

ある例外を別の例外に置き換えたいときは、raise ... from e を使います。元のエラーがチェーンとして保持され、トレースバックには両方が表示されます。

def load_config(path):
    try:
        with open(path) as f:
            return f.read()
    except FileNotFoundError as e:
        raise RuntimeError(f"設定ファイルがありません: {path}") from e

add_note:エラーに文脈を追加する

Python 3.11(PEP 678)からは、add_note() メソッドで例外に注釈を追加し、そのまま再送出できます。例外の型は変わらず、注釈はメッセージの下に表示されます。

def parse(raw, line_no):
    try:
        return float(raw)
    except ValueError as e:
        e.add_note(f"ファイル expenses.csv、{line_no}行目")
        raise

parse("百", 17)
# ValueError: could not convert string to float: '百'
# ファイル expenses.csv、17行目

Pythonで独自の例外を作る

独自の例外は、業務ドメインのエラーを表すために使います。例えば、予算超過、支払い済みの注文、ブロックされたユーザーなどです。Exception を継承したクラスを作るだけで十分です。

class BudgetExceeded(Exception):
    def __init__(self, category, limit, spent):
        super().__init__(f"{category}: 上限 {limit}、支出 {spent}")
        self.category = category
        self.limit = limit
        self.spent = spent

def check_budget(category, limit, spent):
    if spent > limit:
        raise BudgetExceeded(category, limit, spent)

try:
    check_budget("食費", 10000, 12500)
except BudgetExceeded as e:
    print("警告:", e)   # 警告: 食費: 上限 10000、支出 12500

これで呼び出し側のコードは、予算超過だけを捕まえられ、打ち間違いなどと混同しなくなります。大きなプロジェクトでは、共通の基底クラス(class TrackerError(Exception))を作り、独自の例外をすべてそこから継承させると便利です。そうすれば except TrackerError 1つで、すべてを捕まえられます。継承元は BaseException ではなく、必ず Exception にしてください。

例外グループとexcept*

エラーが同時に複数発生することもあります。例えば、ファイル全体を検査するとき、あるいは asyncio.TaskGroup で並列に実行したタスクの場合です。Python 3.11以降では、それらを ExceptionGroup にまとめ、except* で型ごとに処理できます。

def check_all(rows):
    errors = []
    for r in rows:
        try:
            float(r)
        except ValueError as e:
            errors.append(e)
    if errors:
        raise ExceptionGroup("ファイルのエラー", errors)

try:
    check_all(["1", "a", "b"])
except* ValueError as eg:
    print(f"不正な行の数: {len(eg.exceptions)}")   # 不正な行の数: 2

try exceptですべてのエラーを捕まえてはいけない理由

最も有害な習慣は、何でも捕まえてしまうことです。except: pass と書くと、プログラムは「クラッシュしなくなります」が、実際には問題を知らせなくなるだけです。引数なしの except は KeyboardInterrupt や SystemExit まで捕まえるため、Ctrl+C でプログラムを止められなくなります。except Exception: pass は少しましですが、それでも名前の打ち間違いや論理エラーを黙って飲み込んでしまいます。

Pythonで例外を正しく処理するには、次のようにします。

  • 処理できる型だけを捕まえます。すべてではなく、例えば except ValueError です。
  • try のブロックは短くします。その型で失敗しそうな行だけを入れましょう。
  • プログラムの最上位で広く捕まえる(except Exception)場合は、必ずトレースバックをログに残します。
  • 単純な分岐に例外を使わないようにします。簡単な場合は try/except KeyError より dict.get() で十分です。
  • リソースには with を使います。エラーが起きても、ファイルを確実に閉じてくれます。

まとめ

Pythonの例外は、エラーを知らせるための仕組みであり、隠すためのものではありません。tracebackは下から上へ読み、try/except では具体的な型だけを捕まえましょう。else は「うまくいった場合」のコードに、finally は後片付けに使います。Pythonの raise で自分からエラーを発生させられ、raise from と add_note で文脈を残せます。独自の例外は Exception から継承して作ります。講座では、このテーマを実践で扱っています。家計簿ツールが、CSVの1行の不正な値で止まらないようにし、予算超過を独自の例外で表現します。

よくある質問

エラーと例外の違いは何ですか?

日常会話では同じ意味で使われます。厳密には、SyntaxError はコードの解析時に起きるもので、それ以外のエラーは実行時に例外オブジェクトとして発生し、捕まえられるものです。

例外が発生した場所から、処理を続けることはできますか?

いいえ。捕まえた後は、エラーが起きた行ではなく、try/except ブロックの後から実行が続きます。ファイルの各行を処理したいなら、try をループの中に置きましょう。

except の中で例外が起きたらどうなりますか?

その新しい例外が上位へ送られ、元の例外は __context__ 属性に保存されます。トレースバックには「During handling of the above exception, another exception occurred」という文が表示されます。

テストで、関数が例外を発生させることを確かめるにはどうしますか?

pytest では with pytest.raises(ValueError): を使い、unittest では self.assertRaises(ValueError) を使います。ブロックの中で指定した型の例外が起きた場合にのみ、テストが通ります。

assert と raise の違いは何ですか?

assert 条件, "メッセージ" は、条件が偽のときに AssertionError を送出します。開発中のチェック用の仕組みです。-O オプションで起動すると assert は無効になるため、ユーザーの入力の検証は if と raise で行いましょう。

動画でこのテーマを学ぶ

このテーマはコース「Python 3.15: от новичка до профи」で実践とともに詳しく解説しています。

コースへ