Pythonの例外:try、except、else、finally、raise、独自の例外
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 で行いましょう。