Pythonでは実行中の誤り(例外)が起こるとプログラムは停止します。
初心者のうちは原因が不明で困りがちですが、エラーの種類に応じて処理を分ければ安全に回復できます。
本記事では複数exceptで例外ごとに挙動を変える基本とコツを、動くサンプルとともに丁寧に解説します。
複数exceptでエラー種類ごとに分ける基礎
目的とメリット(エラー処理の分岐)
例外は発生した時点で通常の処理が中断されます。
ここでtry-except
を使うと、発生した例外の種類に応じて分岐できます。
例えば、ユーザーの入力ミスは再入力を促し、ネットワーク障害はリトライし、プログラミングのバグは即座に失敗させる、といったきめ細かな対応が可能です。
これにより、ユーザー体験の向上、ログの精度向上、リカバリの容易化といったメリットが得られます。
try-exceptの基本構文
もっとも基本的な構文は次のとおりです。
複数のexcept
を並べれば、例外ごとに別の処理を書けます。
# 例: 複数のexceptで例外を種類ごとに分ける
def parse_and_divide(a_text: str, b_text: str) -> float | None:
try:
# 文字列を数値に変換
a = float(a_text)
b = float(b_text)
# 0で割ると ZeroDivisionError
return a / b
except ValueError as e:
# 数値以外が入っていたなど、値として不正
print(f"[ValueError] 数値に変換できません: {e}")
return None
except ZeroDivisionError as e:
# 0割のときの処理
print(f"[ZeroDivisionError] 0では割れません: {e}")
return None
except Exception as e:
# 想定外の例外は最後にまとめて捕捉
print(f"[想定外] {type(e).__name__}: {e}")
return None
上の例のポイントは、具体的な例外から順に並べ、もっとも広いException
は最後に置くことです。
これにより、より詳細なハンドリングが優先され、想定外はログに残して早期に気づけます。
よく使う例外名
Pythonの例外は非常に多いですが、実務と学習の初期段階で頻出のものを先に押さえましょう。
下の表は初心者がまず覚えると良い例外と、典型的な発生シーンの要約です。
例外名 | 主な意味 | 典型シーン |
---|---|---|
ValueError | 値の不正 | 文字列を数値に変換、範囲外の値 |
TypeError | 型の不正 | 関数に予期しない型、足し算に文字列と数値 |
KeyError | 辞書のキー欠如 | dict の存在しないキー参照 |
IndexError | 範囲外のインデックス | リストの添字が大きすぎる |
ZeroDivisionError | 0割 | 除算の分母が0 |
FileNotFoundError | ファイルが存在しない | open() でパスが間違い |
PermissionError | 権限不足 | ファイルやフォルダの読み書き不可 |
ValueError
値の中身が不正なときに発生します。
例えばint("abc")
や、必要な範囲外の値を渡したときです。
入力チェックや再入力の促しと相性が良いです。
KeyError
辞書にそのキーが無いときに発生します。
my_dict["name"]
のような添字アクセスで起こり、dict.get("name")
なら例外にはなりません。
ZeroDivisionError
分母が0のときの除算で発生します。
ユーザー入力で除算する前に、0でないかのチェックを入れるか、例外で分岐します。
FileNotFoundError
指定したファイルが見つからないときに発生します。
パスの誤り、未作成のファイル、作業ディレクトリの勘違いなどが原因です。
サンプルコードで理解(複数except)
入力エラー(ValueError)とゼロ割(ZeroDivisionError)を分ける
ユーザー入力のバリデーションでは、値が数値かどうかと、分母が0かどうかを個別に扱うとユーザーフレンドリーです。
# 例: 入力エラーと0割を分けて扱う
def safe_divide_interactive() -> None:
# 入力を受け取る
x = input("分子を入力してください: ")
y = input("分母を入力してください: ")
try:
num = float(x) # 数値変換に失敗すると ValueError
den = float(y)
result = num / den # 分母が0だと ZeroDivisionError
except ValueError as e:
print(f"数値以外が入力されました。もう一度入力してください: {e}")
except ZeroDivisionError:
print("分母が0です。0以外の数を入力してください。")
else:
# 例外が発生しなかったときのみ結果を表示
print(f"結果は {result} です。")
# スクリプトとして実行する場合
# safe_divide_interactive()
分子を入力してください: 10
分母を入力してください: 0
分母が0です。0以外の数を入力してください。
分子を入力してください: 10
分母を入力してください: two
数値以外が入力されました。もう一度入力してください: could not convert string to float: 'two'
分子を入力してください: 10
分母を入力してください: 2
結果は 5.0 です。
このように同じ入力処理でも例外ごとに適切なメッセージを出し分けられます。
辞書のKeyErrorと型エラー(TypeError)を分ける
辞書を期待している関数に、誤ってNone
や文字列を渡すバグはよく見かけます。
キーが無い場合と、そもそも辞書ではない場合を分けると原因が明確になります。
# 例: KeyError と TypeError を分ける
def read_age(record, key="age") -> int | None:
"""
record: ユーザー情報を想定。辞書であることを期待する。
key: 年齢のキー名(既定は "age")
"""
try:
# record が辞書以外ならここで TypeError が発生する可能性がある
age_value = record[key] # キーが無い場合は KeyError
return int(age_value) # "25" のような文字列でも int にできる
except KeyError as e:
print(f"[KeyError] 指定キーが見つかりません: {e}")
return None
except TypeError as e:
print(f"[TypeError] 辞書ではないオブジェクトが渡されました: {e}")
return None
# 利用例
# print(read_age({"name": "Taro", "age": "25"})) # 25 が返る
# print(read_age({"name": "Taro"})) # KeyError の扱い
# print(read_age(None)) # TypeError の扱い
25
[KeyError] 指定キーが見つかりません: 'age'
None
[TypeError] 辞書ではないオブジェクトが渡されました: 'NoneType' object is not subscriptable
None
ここでは辞書の設計ミスとデータ欠落を区別して報告できています。
なおdict.get(key)
を使えば例外ではなくNone
が返るため、例外で分岐したい場合は[]
アクセスを使います。
ファイルのエラー(FileNotFoundError/PermissionError)を分ける
ファイル操作では「無い」のか「権限が無い」のかで対処が異なります。
以下は2つを分けて扱う例です。
# 例: FileNotFoundError と PermissionError を分ける
from pathlib import Path
import os
import stat
def read_text(path_str: str) -> str | None:
path = Path(path_str)
try:
with path.open("r", encoding="utf-8") as f:
data = f.read()
return data
except FileNotFoundError as e:
# ファイルが存在しない
print(f"[FileNotFoundError] ファイルが見つかりません: {e.filename}")
return None
except PermissionError as e:
# パスはあるが権限が足りない
print(f"[PermissionError] 権限がありません: {e.filename}")
return None
# 動作例(環境によって PermissionError の再現性は異なります)
# 1) 存在しないパス
# read_text("no_such_file.txt")
# 2) PermissionError を試す簡易例(UNIX系で再現しやすい)
# secure_dir = Path("secure_dir")
# secure_dir.mkdir(exist_ok=True)
# # ディレクトリの読み取り権限を外す(UNIX想定)
# os.chmod(secure_dir, 0)
# try:
# read_text(secure_dir / "any.txt") # ディレクトリに入れず PermissionError の可能性
# finally:
# # 後片付け: 権限を戻して削除
# os.chmod(secure_dir, stat.S_IRWXU)
# secure_dir.rmdir()
[FileNotFoundError] ファイルが見つかりません: no_such_file.txt
[FileNotFoundError] ファイルが見つかりません: secure_dir\any.txt
権限エラーの再現はOSやファイルシステムに依存します。
再現が難しい場合は、権限の無い共有フォルダやシステムディレクトリを参照したときに同様の例外が出る点だけ押さえておきましょう。
exceptの書き方のコツ
具体的な例外を先に書く
より具体的な例外を上に、より広い例外を下に並べます。
先に広いException
を書いてしまうと、後ろの具体的なexcept
は到達しません。
# 悪い例: 先に Exception を書くと後続の except は実行されない
try:
int("abc")
except Exception:
print("ここで止まるので、下の ValueError は使われない")
except ValueError:
print("この行は実行されない")
# 良い例: 具体的な例外から順に
try:
int("abc")
except ValueError:
print("ValueError 個別の対応")
except Exception:
print("最後の安全網")
ここで止まるので、下の ValueError は使われない
ValueError 個別の対応
広い例外(Exception)は最後に
想定外の例外はログに残して早期発見したいので、広いException
は最後に置きます。
ユーザーには一般的なメッセージを見せ、詳細はログに残すのが現実的です。
# 例: 想定外は最後でまとめて捕捉
def do_work():
try:
# ここに本処理
pass
except SpecificError: # 任意の例外とします
# 具体的な回復処理
pass
except Exception as e:
# 予期せぬ失敗はログへ(ここでは print 例示)
print(f"[想定外] {type(e).__name__}: {e}")
raise # 重要: 上位に伝えて失敗に気づけるようにする
複数の例外をまとめて処理(except (ValueError, TypeError))
同じ対処でよい例外はタプルでまとめると、重複コードを減らせます。
# 例: 変換系のエラーをまとめる
def to_int_plus_one(x):
try:
return int(x) + 1
except (ValueError, TypeError) as e: # まとめて同じ扱い
print(f"数値にできない入力です: {type(e).__name__} -> {e}")
return None
# print(to_int_plus_one("10")) # 11
# print(to_int_plus_one("ten")) # None (ValueError)
# print(to_int_plus_one(None)) # None (TypeError)
11
数値にできない入力です: ValueError -> invalid literal for int() with base 10: 'ten'
None
数値にできない入力です: TypeError -> int() argument must be a string, a bytes-like object or a real number, not 'NoneType'
None
例外オブジェクトでメッセージ表示(as e)
例外オブジェクトをas e
で受けると、メッセージや種類を詳細に扱えます。
# 例: 例外名とメッセージを整形して出す
def show_error_detail():
try:
{}["missing"]
except KeyError as e:
name = type(e).__name__
print(f"例外名: {name}, メッセージ: {e}")
# show_error_detail()
例外名: KeyError, メッセージ: 'missing'
素のexceptは避ける
素のexceptは避けましょう。
KeyboardInterrupt
(ユーザーが中断)やSystemExit
まで飲み込んでしまい、プログラムが止められなくなることがあります。
# 悪い例: すべてを飲み込む
try:
...
except:
pass # 何が起きても黙る -> バグの温床
# 良い例: 期待する例外だけ捕捉
try:
...
except (ValueError, KeyError) as e:
print(f"想定した失敗: {e}")
チェックリストとベストプラクティス
捕まえる例外は最小限に
捕捉する例外は、その場で回復できるものに限定します。
処理できないのにとりあえず捕まえるのは、障害の発見を遅らせるだけです。
呼び出し元での再試行やフォールバックがある場合のみ捕捉を検討します。
想定外は落としてバグを見つける
想定外まで握り潰すと、表面上は動いているように見えて静かに壊れ続ける危険があります。
最後のexcept Exception
でログを残し、必要ならraise
で再送出して異常終了させ、早期に修正しましょう。
ユーザー表示とログを分ける
ユーザーには簡潔で状況が分かるメッセージを見せ、技術的な詳細はログに残します。
ユーザー向けには「ネットワークに接続できません。時間をおいて再試行してください。」、ログには例外名・スタックトレース・入力値などの文脈を保存する、といった住み分けが重要です。
テストで例外パターンを確認
ユニットテストでは正常系と例外系の両方を用意します。
入力値の境界(空文字、0、極端な値)、辞書の欠落キー、存在しないファイルなどのケースをカバーすると、実運用での不意の停止を減らせます。
例外が投げられること自体を期待するテスト(pytest.raises
など)も有効です。
まとめ
本記事では、複数exceptで例外の種類ごとに処理を分ける方法を、基本構文から具体例(ValueError/ZeroDivisionError、KeyError/TypeError、FileNotFoundError/PermissionError)まで丁寧に説明しました。
要点は、具体的な例外を先に、広い例外は最後に、同じ対処ならまとめる、素のexceptは避けるの4点です。
これらを守ることで、ユーザーに優しく、原因究明しやすい堅牢なエラー処理が実現できます。
まずは身近な入出力処理から、例外ごとにメッセージを出し分けることから始めてみてください。