これからPython学習を始めるにあたって、どうしても様々なパターンのエラーに出会います。
- Pythonにおけるエラー処理の解決方法を知りたい人
- Pythonにおける代表的なエラー一覧を確認したい人
- 例外処理のエラーハンドリング(ベストプラクティス)を知りたい人
上記の悩みを解決しながら、Pythonにおけるエラー処理と解決方法を解説します。
Pythonにおけるエラー処理
Pythonにおける一般的なエラー処理は、2種類に分けられます。
- 構文エラー(Syntax Error)
- 例外エラー(Exception)
Pythonによるエラー処理として、構文として誤っているものは「構文エラー」、構文として正しいが実行中に発生するエラーは例外と呼ばれます。
また、以下は構文エラーと例外(ランタイムエラー)の違いを比較表にまとめています。
項目 | 構文エラー | 例外エラー |
---|---|---|
検出タイミング | コンパイル(パース)段階で検出。 ソースコードがPython文法に沿っていない時は通常プログラムは開始しない。 | 実行時(ランタイム)で発生。 プログラムが実行中に予期しない事象が起きた際に発生。 |
捕捉(try/except)可能性 | 通常の静的ソース内ではtry/exceptで捕まえられない(コードがそもそも実行されない)。 ただし、compile()/exec()で動的に文字列を評価する場合はSyntaxErrorをtry/exceptで捕捉できる。 | try/exceptで捕捉・処理できる。 |
原因の性質 | コードの書き方が間違っている(タイプミス/コロン忘れ/括弧の有無など)。 | 実行環境やデータの問題(ゼロ除算/ファイルが無い/型が違う/外部入力が不正など)。 |
復旧のしやすさ | 修正して再実行するしかない(ソースの修正)。 | try/exceptでロジックを追加して復旧・再試行したり、ユーザーに入力を促したりできる。 |
スタックトレースの意味 | 通常はファイル名・行番号・構文エラーのメッセージのみ(解析時の情報)。 | トレースバックが出力され、どの関数呼び出しで例外が発生したかを辿れる。 |
実用的なチェック方法 | python -m py_compile file.py、あるいはIDEの静的解析(リンター)やsyntax checker。 | 単体テスト/例外ケースを想定したテスト/入力の検証/ログとトレースバックの確認。 |
Pythonにおけるエラー一覧
Pythonにおいて様々なエラーパターンが存在します。
ここでは、Pythonにおける代表的なエラー一覧をまとめています。
エラー名 | 概要 |
---|---|
SyntaxError | Python文法(Syntax)に誤りがある場合に発生 |
NameError | 存在しない(定義されていない)変数名や関数名を使用した際に発生 |
TypeError | 演算や関数が不適切な「型(Type)」のオブジェクトに対して実行された場合に発生 |
ValueError | 関数の引数の「型」は正しいが、その「値(Value)」が不適切である場合に発生 |
AttributeError | オブジェクトが持っていない属性(変数)やメソッド(関数)にアクセスした際に発生 |
KeyError | 辞書(dict)において存在しないキー(Key)を指定し値を取得した際に発生 |
ModuleNotFoundError | importしようとしたモジュールが見つからない場合に発生 |
IndentationError | インデント(字下げ)が正しくない場合に発生 |
IndexError | 範囲外のインデックス(添字)を指定して要素にアクセスした際に発生 |
ZeroDivisionError | 数値をゼロ(0)で割ろうとした際に発生 |
Pythonの代表的なエラーについて、概要/発生パターンと解消方法/サンプルコードを用いて解説します。
SyntaxError
SyntaxErrorは、Python文法(Syntax)に誤りがある場合に発生する最も基本的なエラーです。
コードが実行される前に、Pythonのインタプリタがコードを解析する段階で検出されます。
発生パターン | 説明 | 解消方法 |
---|---|---|
コロン(:)のつけ忘れ | if, for, def, classなどのブロックの始まりでコロンを忘れる。 | 適切な位置にコロンを追加。 |
括弧の閉じ忘れ | (), {}, []などの括弧の対応が取れていない。 | 括弧のペアが正しく閉じられているか確認。 |
クォーテーションの閉じ忘れ | 文字列を定義する際のシングルクォート(”)やダブルクォート(””)を閉じ忘れる。 | 文字列を正しくクォートで囲む。 |
予約語の誤用 | forやifなどPythonで特別な意味を持つ予約語を変数名として使ってしまう。 | 変数名を予約語と重複しない名前に変更。 |
# エラーが発生するコード
age = 20
if age >= 18
print("成人です")
エラーの原因として、if文の条件式の後に必須であるコロン(:)が欠けています。
Pythonの文法は、ifやforなどの制御構文の行末にはコロンが必要です。
解消方法は、if age >= 18の後ろにコロンを追加します。
# 修正後のコード
age = 20
if age >= 18: # コロンを追加
print("成人です")
NameError
NameErrorは、存在しない(定義されていない)変数名や関数名を使用した際に発生します。
発生パターン | 説明 | 解消方法 |
---|---|---|
変数のスペルミス | 変数名を間違ったスペルで記述する。 | 変数名が正しく記述されているか確認する。 |
定義前の変数を使用 | 変数に値を代入する前に、その変数を使おうとする。 | 変数を使用する前に必ず値を代入。 |
クォーテーションのつけ忘れ | 文字列をクォートで囲み忘れ、変数として解釈されてしまう。 | 文字列はシングルクォートかダブルクォートで囲む。 |
# エラーが発生するコード
user_name = "Taro"
print(user_nam)
エラーの原因は、user_nameという変数を定義しましたが、print関数で呼び出す際にuser_namとスペルを間違えています。
Pythonはuser_namという名前の変数を探しますが、見つからないためエラーになります。
解消方法は、print関数で呼び出す変数名を正しいスペルに修正します。
# 修正後のコード
user_name = "Taro"
print(user_name) # スペルを修正
TypeError
TypeErrorは、演算や関数が不適切な「型(Type)」のオブジェクトに対して実行された場合に発生します。
例えば、数値と文字列を足し算しようとするなど型同士の互換性がない操作で起こります。
発生パターン | 説明 | 解消方法 |
---|---|---|
異なる型同士の演算 | 文字列と数値の足し算など、互換性のない型で演算を行う。 | str(), int(), float()などの関数を使って型を揃えてから演算。 |
引数の型が違う | 関数が想定している型とは異なる型の引数を渡す。 | 関数の仕様を確認し、正しい型の引数を渡す。 |
# エラーが発生するコード
age = "20"
print("来年の年齢は " + (age + 1) + " 歳です")
エラーの原因は、変数ageは文字列”20″です。
数値の1を足そうとしています(age + 1)。
Pythonでは文字列と数値を直接+演算子で結合することはできないため、TypeErrorが発生します。
解消方法は、文字列であるageをint()関数を使って数値に変換してから計算します。
# 修正後のコード
age = "20"
# ageをint型に変換してから計算
print("来年の年齢は " + str(int(age) + 1) + " 歳です")
ValueError
ValueErrorは、関数の引数の「型」は正しいものの、「値(Value)」が不適切である場合に発生します。
発生パターン | 説明 | 解消方法 |
---|---|---|
不適切な文字列の型変換 | 数字以外の文字が含まれる文字列をint()やfloat()で数値に変換しようとする。 | 数値に変換する前に、文字列が数字のみで構成されているか確認。 |
リストからの要素削除の失敗 | list.remove()でリスト内に存在しない値を指定する。 | 削除する前に、その値がリストに存在するかin演算子で確認。 |
# エラーが発生するコード
age_str = "二十歳"
age_num = int(age_str)
print(age_num)
エラーの原因は、int()関数は文字列を数値に変換しますが、文字列は”10″や”20″のような数字で構成される必要があります。
“二十歳”という文字列には漢数字や他の文字が含まれているため、数値に変換できずValueErrorが発生します。
解消方法は、int()で変換する文字列は数字のみを含むものにします。
# 修正後のコード
age_str = "20" # 数字のみの文字列に変更
age_num = int(age_str)
print(age_num)
AttributeError
AttributeErrorは、オブジェクトが持っていない属性(変数)やメソッド(関数)にアクセスした際に発生します。
発生パターン | 説明 | 解消方法 |
---|---|---|
メソッド名のスペルミス | オブジェクトのメソッド名を間違えて呼び出す(例:appendをaddと間違える)。 | 正しいメソッド名に修正。 |
異なる型のオブジェクトでメソッドを呼び出す | ある型にしかないメソッドを、別の型のオブジェクトで使おうとする(例:整数型で文字列用のメソッドupper()を使う)。 | メソッドを呼び出すオブジェクトの型を確認し、その型で使えるメソッドを使用。 |
# エラーが発生するコード
my_list = [1, 2, 3]
my_list.add(4)
エラーの原因は、本来Pythonのリスト(list型)に要素を追加するメソッドはappend()です。
add()というメソッドはリストには存在しません(add()はセット型で使われるメソッドです)。
存在しないメソッドを呼び出そうとしたため、AttributeErrorが発生します。
解消方法は、メソッド名をaddからappendに修正します。
# 修正後のコード
my_list = [1, 2, 3]
my_list.append(4) # 正しいメソッド名に修正
print(my_list)
KeyError
KeyErrorは、辞書(dict)において、存在しないキー(Key)を指定して値を取得した際に発生します。
発生パターン | 説明 | 解消方法 |
---|---|---|
キーのスペルミス | 辞書に存在するキーと違うスペルでアクセスする。 | 正しいキーのスペルに修正。 |
存在しないキーへのアクセス | 辞書に登録されていないキーを指定する。 | アクセスする前にin演算子でキーの存在を確認するか、存在しない場合にデフォルト値を返す.get()メソッドを使用。 |
# エラーが発生するコード
user = {"name": "Taro", "age": 25}
print(user["email"])
エラーの原因は、user辞書には”name”と”age”というキーしか存在しません。
存在しないキー”email”にアクセスしようとしたため、KeyErrorが発生します。
解消方法は、存在するキー(”name”や”age”)を指定するか、.get()メソッドを使って安全にアクセスします。
.get()はキーが存在しない場合にNoneまたは指定したデフォルト値を返します。
# 修正後のコード
user = {"name": "Taro", "age": 25}
# .get()メソッドを使うとエラーにならずNoneが返る
print(user.get("email"))
# デフォルト値を指定することも可能
print(user.get("email", "登録されていません"))
ModuleNotFoundError
ModuleNotFoundErrorは、importしようとしたモジュールが見つからない場合に発生します。
発生パターン | 説明 | 解消方法 |
---|---|---|
モジュール名のスペルミス | importするモジュール名を間違える。 | 正しいモジュール名に修正。 |
モジュールがインストールされていない | pipなどでインストールが必要な外部ライブラリを、インストールせずにimportしようとする。 | ターミナル(コマンドプロンプト)でpip install <モジュール名>を実行してインストール。 |
# エラーが発生するコード
import request
エラーの原因は、HTTPリクエストを扱うライブラリはrequestsですが、request(最後のsがない)とスペルを間違えてimportしようとしています。
requestという名前のモジュールは見つからないため、ModuleNotFoundErrorが発生します。
解消方法は、正しいモジュール名requestsに修正します。
もしrequests自体がインストールされていない場合は、pip install requestsでインストールが必要です。
# 修正後のコード
import requests # 正しいモジュール名に修正
IndentationError
IndentationErrorは、インデント(字下げ)が正しくない場合に発生します。
Pythonではインデントがコードのブロック構造を定義しているためです。
発生パターン | 説明 | 解消方法 |
---|---|---|
不要な場所でのインデント | ブロックの先頭でもないのに、行頭に不要なスペースやタブを入れる。 | 不要なインデントを削除。 |
インデントが不足している | if文やfor文、関数定義などの後、本来インデントが必要なブロックでインデントを忘れる。 | 適切なインデント(通常は半角スペース4つ)を追加。 |
インデントの深さが混在 | 同じブロック内で、異なる数のスペースやタブが混在している。 | ブロック内のインデントを統一(例:全て半角スペース4つ)。 |
# エラーが発生するコード
def say_hello():
print("こんにちは")
エラーの原因は、defで関数を定義した後、関数ブロック内のコードはインデントする必要があります。
print(“こんにちは”)がインデントされていないため、構文エラーになりIndentationErrorが発生します。
解消方法は、print文の前に適切なインデント(半角スペース4つ)を追加します。
# 修正後のコード
def say_hello():
print("こんにちは") # インデントを追加
IndexError
IndexErrorは、リストやタプルなどのシーケンス型において範囲外のインデックス(添字)を指定して要素にアクセスした際に発生します。
発生パターン | 説明 | 解消方法 |
---|---|---|
範囲外のインデックスを指定 | シーケンスの要素数以上のインデックスを指定する。 Pythonのインデックスは0から始まることに注意が必要。 | インデックスが0からlen(シーケンス) – 1の範囲内にあるか確認。 |
空のシーケンスへのアクセス | 要素が一つもない空のリストなどに対して、インデックス(例:[0])でアクセスしようとする。 | アクセスする前に、シーケンスが空でないか確認。 |
# エラーが発生するコード
fruits = ["りんご", "みかん", "バナナ"]
print(fruits[3])
エラーの原因は、fruitsリストには3つの要素がありますが、インデックスは0(りんご)、1(みかん)、2(バナナ)までです。
存在しないインデックス3にアクセスしようとしたため、IndexErrorが発生します。
解消方法は、0から2までの有効なインデックスを指定します。
# 修正後のコード
fruits = ["りんご", "みかん", "バナナ"]
print(fruits[2]) # 範囲内のインデックスを指定
ZeroDivisionError
ZeroDivisionErrorは、数値をゼロ(0)で割ろうとした際に発生する数学的な定義に基づいたエラーです。
発生パターン | 説明 | 解消方法 |
---|---|---|
ゼロ除算 | /(除算)や%(剰余)演算子で、右側の値(除数)がゼロになる。 | 計算を実行する前に、除数がゼロでないことをif文などで確認。 |
# エラーが発生するコード
numerator = 100
denominator = 0
result = numerator / denominator
print(result)
エラーの原因は、変数denominatorの値が0であり、100/0というゼロでの割り算を実行しています。
数値はゼロで割ることができないため、ZeroDivisionErrorが発生します。
解消方法は、割り算を行う前に分母(除数)が0でないかチェックする処理を追加します。
# 修正後のコード
numerator = 100
denominator = 0
if denominator != 0:
result = numerator / denominator
print(result)
else:
print("エラー: 0で割ることはできません。")
Pythonにおける例外処理のエラーハンドリング集
ここでは、Pythonにおける例外処理のハンドリング集を掲載します。
以下は、例外処理に関する代表的なエラーハンドリングになります。
- 処理可能な例外のみをキャッチする
- 無条件のexcept句を避ける
- try-except-else-finally ブロックを使用する
- 例外をログに記録する
- 必要に応じて例外をraiseする
- リソース管理にはコンテキストマネージャを使用する
- デグレードを実装する
- 例外を無視しない
- 例外をドキュメント化する
- 適切な場合にはカスタム例外を使用する
- 例外処理をテストする
- 例外の多用を避ける
- コンテキストを保持するために例外をリンクする
Pythonにおける例外処理のベストプラクティスについて、ハンドリングの説明/サンプルコードと解説/使用上の注意点をまとめています。
処理可能な例外のみをキャッチする
except句では、その場で具体的かつ適切に対処できる例外のみをキャッチするべきです。
例えば、ユーザーの入力ミスが原因で発生するValueErrorをキャッチし、再入力を促すのは良い例です。
しかし、対処方法がわからない例外(例: MemoryError)をキャッチしても、プログラムの状態が不安定になり意味がありません。
# 悪い例
try:
age_str = input("年齢を入力してください: ")
age = int(age_str)
except Exception as e: # 広すぎる例外キャッチ
print(f"予期せぬエラーが発生しました: {e}")
# 良い例
try:
age_str = input("年齢を入力してください: ")
age = int(age_str)
print(f"あなたは{age}歳です。")
except ValueError: # 処理可能なValueErrorに限定
print("エラー: 数値を入力してください。")
悪い例ではExceptionという非常に広範な例外をキャッチしています。
ValueErrorだけでなく、ユーザーがCtrl+Cを押した際のKeyboardInterruptなど本来プログラムを終了させるべき例外まで握りつぶしてしまいます。
良い例では、int()への変換失敗時に発生することが予測されるValueErrorのみをキャッチしています。
想定内のエラーのみ対処し、予期せぬエラーはプログラムを停止させるなど上位の処理に委ねることができます。
広すぎる例外のキャッチは、バグの発見を遅らせる原因になります。
具体的な例外クラスを指定することで、コードの意図が明確になり堅牢性も向上します。
無条件のexcept句を避ける
例外クラスを省略した無条件のexcept:句(Bare Except)は絶対に避けるべきです。
BaseExceptionをキャッチするため、SystemExit(sys.exit())やKeyboardInterrupt(Ctrl+C)といったプログラムの終了に関わる例外まで補足します。
プログラムを正常に終了できなくなる可能性があります。
# 絶対に避けるべき例
try:
# 何らかの処理
user_input = input()
result = 1 / int(user_input)
except: # 無条件のexcept句
print("何らかのエラーが発生しました。")
ユーザーがCtrl+Cを押してプログラムを中断しようとしても、except:ブロックがKeyboardInterruptをキャッチしてしまい、プログラムは終了しません。
ユーザーを混乱させてアプリケーションを制御不能な状態に陥れる可能性があります。
最低でもexcept Exception as e:と書くべきですが、具体的な例外をキャッチすることが推奨されます。
無条件のexcept:句は、デバッグを極めて困難にします。
try-except-else-finallyブロックを使用する
以下は、例外処理に利用するtry-except-else-finallyブロックの説明になります。
try | 例外が発生する可能性のあるコードを記述 |
---|---|
except | tryブロック内で例外が発生した場合に実行 |
else | tryブロック内で例外が発生しなかった場合に実行 |
finally | tryブロックで例外が発生したかどうかに関わらず必ず最後に実行 |
else句を使うことで、tryブロックを「例外が発生しうる最小限のコード」に保ち可読性が向上します。
finally句は、ファイルやネットワーク接続のクローズなど後始末処理に不可欠です。
f = None # finallyで参照できるよう、tryの外で定義
try:
f = open('data.txt', 'r', encoding='utf-8')
content = f.read()
except FileNotFoundError:
print("ファイルが見つかりません。")
except UnicodeDecodeError:
print("ファイルのエンコードが不正です。")
else:
# 例外が発生しなかった場合にのみ実行
print("ファイルの内容を正常に読み込みました。")
print(content)
finally:
# 成功・失敗に関わらず必ず実行
if f:
f.close()
print("ファイルをクローズしました。")
ファイル読み込みの一連の処理をtry-except-else-finallyで構造化しています。
ファイルが存在しない場合はFileNotFoundErrorが処理されます。
ファイルは存在するが文字コードが違う場合はUnicodeDecodeErrorが処理されます。
正常に読み込めた場合のみelseブロックが実行され、内容が表示されます。
どのルートを辿っても、finallyブロックでファイルのクローズ処理が試みられます。
else句は、例外が発生しなかった場合にのみ実行したい後続処理(成功時の処理)を記述するのに便利です。
tryブロックの責務が明確になります。
例外をログに記録する
例外をキャッチした際、ユーザーに簡単なメッセージを見せるだけでなく開発者へ詳細な情報をログに記録することが重要です。
ログには、エラーの種類/メッセージ/発生時刻/スタックトレースを含めるべきです。
本番環境で発生した問題のデバッグが格段に容易になります。
import logging
# ロギングの基本設定
logging.basicConfig(
level=logging.ERROR,
filename='app.log',
format='%(asctime)s - %(levelname)s - %(message)s'
)
def get_inverse(n):
try:
return 1 / n
except ZeroDivisionError as e:
# logging.exceptionはスタックトレースも記録してくれる
logging.exception("ゼロ除算エラーが発生しました。入力値: %s", n)
# ユーザーには簡単なメッセージを返す or Noneを返す
return None
get_inverse(0)
2025-09-30 23:17:00,123 - ERROR - ゼロ除算エラーが発生しました。入力値: 0
Traceback (most recent call last):
File "my_script.py", line 12, in get_inverse
return 1 / n
ZeroDivisionError: division by zero
print()でエラーを表示する代わりに、標準ライブラリのloggingモジュールを使用しています。
logging.exception()をexceptブロック内で使うと、エラーメッセージに加えてスタックトレースが自動的にログファイルに出力されます。
後から「いつ、どこで、なぜ」エラーが起きたかを正確に追跡できます。
本番環境では、ログのレベル(DEBUG, INFO, WARNING, ERROR, CRITICAL)を適切に設定し、運用に必要な情報を記録するよう設定することが重要です。
必要に応じて例外をraiseする
ある関数で例外をキャッチしたものの、その場で完全には対処できない場合があります。
例えば、リソースのクリーンアップだけ行い、エラーが発生した事実は呼び出し元に通知したい場合などです。
このような場合は、例外を再度raise(再送出)します。
また、関数の引数が不正である場合など自ら例外を発生させるコードの堅牢性を高めます。
def process_user_data(user_id):
db_conn = None
try:
db_conn = connect_to_db()
# ... データベースからユーザーデータを取得する処理 ...
if user_id < 0:
raise ValueError("ユーザーIDは正の数である必要があります。")
except DatabaseError as e:
print("データベース接続エラー。クリーンアップします。")
# ここでは対処できないので、呼び出し元に通知する
raise
finally:
if db_conn:
db_conn.close()
try:
process_user_data(123)
except DatabaseError:
print("アプリケーションレベルのエラー: データベース処理に失敗しました。")
raise ValueError(…)は、user_idが不正な場合に関数がValueErrorを発生させています。
不正な値で処理が進むことを防ぎます。
raiseは、except DatabaseErrorブロック内でキャッチした例外をクリーンアップ後にそのまま再送出しています。
低レベルのエラー情報(スタックトレースなど)を失うことなく、上位の関数(呼び出し元)にエラー処理を委ねることができます。
引数なしのraiseは、キャッチした例外をそのまま再送出します。
raise eのように書くとスタックトレースが一部変わることがあるため、通常は引数なしのraiseが推奨されます。
リソース管理にはコンテキストマネージャを使用する
ファイル/ネットワーク接続/DB接続/ロックなど、使用後に必ず「閉じる」「解放する」といった後始末が必要なリソースを扱う場合、コンテキストマネージャ(with文)を使用するケースがあります。
withブロックを抜ける際に、エラーが発生したかどうかに関わらず後始末処理が自動的に呼び出されます。
# 悪い例 (finallyを書き忘れる可能性がある)
f = open('data.txt', 'w')
try:
f.write('hello')
finally:
f.close()
# 良い例 (簡潔で安全)
try:
with open('data.txt', 'w', encoding='utf-8') as f:
f.write('こんにちは、世界!')
# withブロックを抜けると自動的にf.close()が呼ばれる
except IOError as e:
print(f"ファイル書き込みエラー: {e}")
with文は、open()が返すファイルオブジェクトの__enter__メソッドと__exit__メソッドを利用します。
withブロックの開始時に__enter__、終了時に__exit__が(後始末処理として)自動で呼び出されます。
close()の呼び忘れがなくなりコードも簡潔になります。
with文は、コンテキストマネージャプロトコルを実装したオブジェクトのみ使用可能です。
自作クラスでこの仕組みを利用したい場合は、__enter__と__exit__メソッドを実装します。
デグレードを実装する
グレースフル・デグラデーション(Graceful Degradation)とは、システムの一部(制御不能な要素)で障害が発生した際にシステム全体が停止するのではなく機能を限定し動作させる設計思想です。
例えば、外部APIから追加情報を取得できない場合も、必須情報だけでページを表示するあるいはキャッシュされた古いデータを表示するなど対応が考えられます。
import requests
CACHE = {}
def get_weather_data(city):
"""天気情報をAPIから取得する。失敗した場合はキャッシュを返す。"""
try:
# 外部APIにアクセス(失敗する可能性がある)
url = f"https://api.weather.com/data?city={city}"
response = requests.get(url, timeout=3.0)
response.raise_for_status() # 200番台以外のステータスコードで例外を発生
data = response.json()
CACHE[city] = data # 成功したらキャッシュを更新
return data
except requests.exceptions.RequestException as e:
print(f"警告: 天気APIへのアクセスに失敗しました - {e}")
# APIが失敗しても、キャッシュにあれば古いデータを返す(デグレード)
if city in CACHE:
print("キャッシュされたデータを返します。")
return CACHE[city]
else:
# キャッシュもなければ、デフォルト値を返す
return {"temperature": "N/A", "description": "取得できません"}
# APIが正常でも、ダウンしていても、アプリケーションは動き続ける
weather = get_weather_data("Tokyo")
print(f"現在の気温: {weather['temperature']}")
get_weather_data関数は、天気APIへのアクセスをtryブロック内で行います。
requests.getがタイムアウトしたり、サーバーが500エラーを返した場合にRequestExceptionが発生します。
exceptブロックでは、アプリケーションをクラッシュさせる代わりにログに警告を出し、以前成功した際に保存したCACHEのデータを返します。
API障害時でもユーザーは(少し古い)情報を得ることができ、アプリケーションの体験が完全に損なわれることを防ぎます。
どの機能が「必須」で、どの機能が「代替可能」かを事前に設計しておくことが重要です。
デグレードした際には、その旨をユーザーに通知するかあるいはログに記録しておくべきです。
例外を無視しない
exceptブロックで例外をキャッチしたが、何も処理をしない(pass文だけを置く)のは危険な行為の一つです。
「サイレントエラー」と呼ばれ、問題が発生したのに表面上は正常に動作しているように見えるため、バグの原因究明が困難になります。
import json
# 絶対にやってはいけない例
def load_config(path):
try:
with open(path, 'r') as f:
return json.load(f)
except Exception:
pass # エラーを完全に無視!
config = load_config('invalid.json')
# configはNoneになり、後続の処理で必ずエラーになる
print(config['host']) # -> TypeError: 'NoneType' is not subscriptable
load_config関数がファイル読み込みやJSONパースに失敗しても、except: passによってエラーが完全に握りつぶされます。
関数はNoneを返し、呼び出し元は設定が読み込めていないことに気づきません。
全く関係ないと思われる箇所(config[‘host’])でTypeErrorが発生し、開発者は本来の原因(ファイルが存在しない/JSONの形式が不正など)を見つけるのに多大な時間を費やすことになります。
意図的に例外を無視したい場合(それが設計上正しい場合)は、passの代わりになぜ無視するのかを明確にコメントで記述するべきです。
例外をドキュメント化する
自作関数やクラスが例外を送出する可能性があるか、docstring(ドキュメンテーション文字列)に明記することはAPIの利用者に重要です。
利用者は例外をtry…exceptで処理すべきかを事前に知ることができ、堅牢なコードを書く助けになります。
def get_user_by_id(user_id):
"""指定されたIDのユーザー情報をデータベースから取得する。
Args:
user_id (int): 取得対象のユーザーID。
Returns:
dict: ユーザー情報の辞書。
Raises:
ValueError: user_idが負数または0の場合。
UserNotFoundError: 指定されたIDのユーザーが存在しない場合。
DatabaseError: データベース接続に失敗した場合。
"""
if user_id <= 0:
raise ValueError("user_idは正の整数である必要があります。")
# ... データベース検索処理 ...
# (見つからなければ UserNotFoundError を raise)
# (接続失敗すれば DatabaseError を raise)
関数docstringにはRaises:セクションがあり、送出する可能性のある例外(ValueError, UserNotFoundError, DatabaseError)が明記されています。
開発者は、「get_user_by_idを呼び出すときは、3つの例外を考慮してexcept節を書く必要がある」と判断できます。
GoogleスタイルやNumpyスタイルなど標準的なdocstringのフォーマットに従うと、SphinxなどツールでAPIドキュメントを自動生成する際にも役立ちます。
適切な場合にはカスタム例外を使用する
Pythonの組み込み例外(ValueError, TypeErrorなど)は便利ですが、アプリケーション固有のエラー状態を表すには不十分な場合があります。
例えば、「残高不足」や「API認証失敗」といったドメイン固有の問題は、独自のカスタム例外クラスを定義することで明確に表現できます。
エラー処理のコードが読みやすく、意図が明確になります。
# アプリケーション固有の例外を定義
class InsufficientBalanceError(Exception):
"""残高不足を表す例外クラス。"""
def __init__(self, current_balance, required_amount):
self.current_balance = current_balance
self.required_amount = required_amount
message = f"残高が不足しています。必要額: {required_amount}, 現在の残高: {current_balance}"
super().__init__(message)
def withdraw(balance, amount):
if amount > balance:
raise InsufficientBalanceError(balance, amount)
return balance - amount
# 呼び出し元のコード
try:
current_balance = 1000
withdraw_amount = 1500
new_balance = withdraw(current_balance, withdraw_amount)
except InsufficientBalanceError as e:
print(f"エラー: {e}")
# エラーオブジェクトから詳細情報にアクセスできる
print(f"現在の残高: {e.current_balance}, 要求額: {e.required_amount}")
ValueErrorの代わりに、InsufficientBalanceErrorという具体的な名前のカスタム例外を定義しています。
except InsufficientBalanceError:と書くだけで、「ここでは残高不足のケースを処理している」ということが一目瞭然になります。
また、カスタム例外クラスに属性(current_balanceなど)を持たせることで、エラー発生時の詳細なコンテキストを呼び出し元に渡せます。
関連するカスタム例外が複数ある場合は、共通の基底クラスを作ると便利です。
例外処理をテストする
正常系のコードだけでなく、例外が発生するケースも単体テストの対象にすべきです。
エラーハンドリングのロジックが正しく機能すること、期待通りの例外が適切な条件下で送出されることを保証できます。
pytestやunittestなどのテストフレームワークには、例外の発生を検証する便利な機能が用意されています。
# テスト対象の関数 (前述のwithdraw関数)
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("引き出し額は正の数である必要があります。")
if amount > balance:
raise InsufficientBalanceError(balance, amount)
return balance - amount
# pytestを使ったテストコード
import pytest
from my_module import withdraw, InsufficientBalanceError # 上記関数をインポート
def test_withdraw_insufficient_balance():
# InsufficientBalanceErrorが発生することを検証
with pytest.raises(InsufficientBalanceError):
withdraw(balance=100, amount=200)
def test_withdraw_invalid_amount():
# amountが負数の場合にValueErrorが発生することを検証
with pytest.raises(ValueError, match="引き出し額は正の数"):
withdraw(balance=100, amount=-50)
def test_withdraw_successful():
# 正常系のテスト
assert withdraw(balance=100, amount=70) == 30
pytest.raisesはコンテキストマネージャとして機能し、withブロック内で指定された例外が発生するとテストが成功し、発生しないあるいは別の例外が発生すると失敗します。
「残高が足りない場合にInsufficientBalanceErrorが正しく送出されるか」といった例外処理のロジックを確実にテストできます。
match引数を使うと、エラーメッセージの内容まで検証できます。
例外をキャッチした後のプログラムの状態(例:データベースがロールバックされているか)もテストすることが信頼性の高いエラー処理には不可欠です。
例外の多用を避ける
例外処理は、「例外的」で予期しない状況に対応する仕組みです。
プログラムの正常なロジック(ループの終了条件など)を制御するために例外を使うべきではありません。
コードは読みにくく意図が分かりづらくなり、通常の制御構文(if, forなど)に比べてパフォーマンスも劣ります。
# 悪い例: 例外を制御フローに使用
items = [1, 2, 3]
i = 0
try:
while True:
item = items[i]
print(item)
i += 1
except IndexError:
# ループを終了するためにIndexErrorを"利用"している
pass
# 良い例: 通常の制御フローを使用
for item in items:
print(item)
悪い例では、リストの末尾に到達した際に発生するIndexErrorをwhileループを抜けるための「合図」として利用しています。
これはコードを読む人に「なぜここで例外処理が?」という混乱を与えます。
良い例のように、Pythonが提供するforループを使えば簡潔かつ直感的に同じ処理を記述できます。
Pythonには「EAFP (Easier to Ask for Forgiveness than Permission)」という考え方があります。
例えば、辞書のキー存在確認(try: d[k] vs if k in d:)など特定場面ではtry-exceptが推奨されることもあります。
しかし、「アクセス試行のコストが高い場合」や「成功率が非常に高いと見込まれる場合」に限定されるべきで、一般的な制御フローの代替として使うべきではありません。
コンテキストを保持するために例外をリンクする
ある例外をキャッチし、原因として別の抽象的な例外を発生させたい場合があります。
例えば、低レベルのKeyErrorをビジネスロジックに近いUserDataErrorとしてラップするようなケースです。
元の例外情報を失ってしまうと、根本原因のデバッグが困難になります。
Python3ではraise new_exception from original_exceptionという構文を使うことで、例外をチェーン(連鎖)できます。
class UserDataError(Exception):
"""ユーザーデータ処理中の汎用エラー"""
pass
def get_user_email(user_data: dict):
try:
return user_data["email"]
except KeyError as e:
# KeyErrorを根本原因として、より抽象的な例外を発生させる
raise UserDataError("ユーザー情報にemailフィールドが含まれていません。") from e
# --- 実行 ---
user = {"name": "Taro"}
try:
get_user_email(user)
except UserDataError as e:
print(f"エラー: {e}")
# __cause__属性で元の例外にアクセスできる
print(f"根本原因: {e.__cause__}")
エラー: ユーザー情報にemailフィールドが含まれていません。
根本原因: 'email'
Traceback (most recent call last):
File "my_script.py", line 7, in get_user_email
return user_data["email"]
KeyError: 'email'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "my_script.py", line 15, in <module>
get_user_email(user)
File "my_script.py", line 10, in get_user_email
raise UserDataError("ユーザー情報にemailフィールドが含まれていません。") from e
__main__.UserDataError: ユーザー情報にemailフィールドが含まれていません。
get_user_email関数は、辞書に”email”キーがない場合にKeyErrorをキャッチします。
raise UserDataError(…) from eを使い、元のKeyError(e)を原因とするUserDataErrorを送出しています。
最終的なトレースバックには「KeyErrorが原因でUserDataErrorが発生した」という因果関係が明記されます。
ライブラリやフレームワークを開発する際、内部的な実装の詳細(例: KeyErrorやIndexError)を隠蔽し、抽象的で安定したAPIを提供したい場合に有効なテクニックです。