外部ファイルの欠如やゼロ除算など、ちょっとした出来事でプログラムは簡単に止まってしまいます。
エラーでも止まらないようにするにはtry-except
が鍵です。
この記事ではPython初心者の方に向けて、基本の書き方から安全に継続するパターンまでを、動くサンプルと出力つきで丁寧に解説します。
Pythonのtry-exceptの基本
例外処理でプログラムを止めない仕組み
Pythonでは、実行時に問題が起きると「例外」と呼ばれるイベントが発生し、何もしないとプログラムはそこで停止します。
try
で「エラーが出そうな処理」を囲み、対応するexcept
で受け止めることで、停止せずに先へ進めます。
例外を捕まえると、そのブロック内で代替処理や記録を行い、残りの処理へ継続できます。
# 例: ゼロ除算を捕まえて、エラーメッセージを出してから続行する
print("開始")
try:
x = 1 / 0 # ここで ZeroDivisionError が発生
print("この行は実行されません")
except ZeroDivisionError as e:
# 例外オブジェクト e からメッセージを確認しつつ継続
print(f"エラー発生: {type(e).__name__}: {e} -> でも続行します")
print("終了")
開始
エラー発生: ZeroDivisionError: division by zero -> でも続行します
終了
「止まる」代わりに「受け止めて続ける」ことが、例外処理の本質です。
この流れを体感しておくと、以降の具体例の理解がスムーズになります。
最小の書き方(tryとexceptだけ)
最小構成はtry
とexcept
だけです。
学習段階ではexcept Exception as e
で「とにかく止めない」を体験できます。
ただし運用では後述の注意点に従いましょう。
# 最小の例: まずは「止めない」を体験
def risky():
# ここではわざとエラーを起こします
raise ValueError("サンプルのエラー")
print("処理開始")
try:
risky()
except Exception as e: # 学習用: 例外を広く受ける
print(f"止めずに続行: {type(e).__name__} -> {e}")
print("処理終了")
処理開始
止めずに続行: ValueError -> サンプルのエラー
処理終了
注意: 実務では「なんでもException
」で受けるのは非推奨です。
下の「安全に使うコツ」で理由と改善策を説明します。
最小の形は学習・試作のためにとどめ、本番では例外を絞り込みます。
どこに置くか(エラーが出そうな行を囲む)
tryの範囲は「エラーが出そうな最小の行」だけを囲むのが基本です。
範囲が広いと「どこで失敗したか」が分かりにくくなります。
# 悪い例: tryの範囲が広すぎて、どこで失敗したか分かりづらい
def step1():
print("step1 OK")
def step2():
print("step2 実行")
raise RuntimeError("step2で失敗")
def step3():
print("step3 OK")
try:
step1()
step2() # ここで失敗する
step3() # ここまで来ない
except Exception as e:
print(f"どこで失敗したか特定しづらい: {type(e).__name__}: {e}")
step1 OK
step2 実行
どこで失敗したか特定しづらい: RuntimeError: step2で失敗
# 良い例: 失敗が想定される箇所だけを最小で囲む
def step1():
print("step1 OK")
def step2():
print("step2 実行")
raise RuntimeError("step2で失敗")
def step3():
print("step3 OK")
step1()
try:
step2() # ここだけを最小で囲む
except RuntimeError as e:
print(f"step2だけ捕捉: {type(e).__name__}: {e}")
step3()
step1 OK
step2 実行
step2だけ捕捉: RuntimeError: step2で失敗
step3 OK
「どの行で何が起きたか」を切り分けられるように、tryは最小の範囲で設計します。
すぐ使える書き方とサンプル
代表的なエラーと、止めずに続けるための基本方針を次の表に整理します。
ここでの方針はあくまで一例であり、実際には要件に合わせて調整してください。
例外 | 代表的な原因 | 継続の方針(例) |
---|---|---|
ZeroDivisionError | 0で割る | 0やNoneなどのデフォルト値を返す |
FileNotFoundError | ファイルが存在しない | 空ファイルを作る/スキップする |
KeyError | 辞書にキーがない | デフォルト値で代用する |
IndexError | リストの範囲外アクセス | 該当要素をスキップする |
以下で各ケースを実行可能なサンプルで確認します。
ゼロ除算(ZeroDivisionError)を0で代用
割り算の分母が0になる可能性があるなら、例外を捕まえて0を返すことで、集計などを止めずに続けられます。
# ゼロ除算を0で代用する安全な割り算
def safe_div(num, den):
try:
return num / den
except ZeroDivisionError as e:
print(f"[警告] {type(e).__name__}: {e} -> 0で代用します")
return 0 # デフォルト値で継続
# 動作確認
print(safe_div(10, 2)) # 5.0
print(safe_div(10, 0)) # 0 で代用
print(safe_div(0, 10)) # 0.0
5.0
[警告] ZeroDivisionError: division by zero -> 0で代用します
0
0.0
「エラーでも結果を返す」ことで後続の処理が止まらず、全体の安定性が上がります。
ただし0での代用が妥当かは要件に合わせて判断してください。
ファイルがない(FileNotFoundError)時は作成かスキップ
ファイル読み込みは外部要因に左右されやすいです。
存在しない場合に「作成して続行」または「スキップして続行」の2パターンが現実的です。
# パターンA: なければ作成してから読み込む
from pathlib import Path
path = Path("sample_data.txt")
try:
text = path.read_text(encoding="utf-8")
except FileNotFoundError as e:
print(f"[情報] {type(e).__name__}: {e} -> 新規作成します")
path.write_text("初期データ\n1行目\n", encoding="utf-8")
text = path.read_text(encoding="utf-8")
print("読み込んだ内容:")
print(text)
[情報] FileNotFoundError: [Errno 2] No such file or directory: 'sample_data.txt' -> 新規作成します
読み込んだ内容:
初期データ
1行目
# パターンB: なければスキップして他の処理を続ける
from pathlib import Path
def process_optional_file(p: Path):
try:
print("任意ファイルの1行目:", p.read_text(encoding="utf-8").splitlines()[0])
except FileNotFoundError as e:
print(f"[注意] {type(e).__name__}: {e} -> このファイルはスキップします")
process_optional_file(Path("optional_missing.txt"))
print("スキップ後も他の処理を継続できます")
[注意] FileNotFoundError: [Errno 2] No such file or directory: 'optional_missing.txt' -> このファイルはスキップします
スキップ後も他の処理を継続できます
「足りなければ作る」「重要度が低ければ飛ばす」といった判断を明確化すると、設計が安定します。
辞書キーがない(KeyError)はデフォルト値で続行
辞書アクセスはキー欠落でKeyError
になります。
try-exceptで捕まえてデフォルト値をあてれば、安全に前へ進めます。
# プロファイルから年齢を取得。なければ "unknown" で続行
profile = {"name": "Taro"} # "age" が無い
try:
age = profile["age"]
except KeyError as e:
print(f"[注意] {type(e).__name__}: {e} -> デフォルト値で続行")
age = "unknown"
print(f"name={profile['name']}, age={age}")
[注意] KeyError: 'age' -> デフォルト値で続行
name=Taro, age=unknown
補足: 辞書はdict.get("age", "unknown")
で例外なくデフォルト値を返せます。
ただし、「なぜ無いのか」をログに残したい場合は、あえてtry-except
でメッセージを出す設計も有効です。
リスト範囲外(IndexError)は処理をスキップ
無効な添字アクセスはスキップして、処理を続けます。
data = ["A", "B", "C"]
indices = [0, 2, 5, 1] # 5 は範囲外
for idx in indices:
try:
item = data[idx]
print(f"index={idx} -> {item}")
except IndexError as e:
print(f"[注意] {type(e).__name__}: {e} -> index={idx} はスキップ")
index=0 -> A
index=2 -> C
[注意] IndexError: list index out of range -> index=5 はスキップ
index=1 -> B
「不正な要素だけ飛ばす」ことで、全体の処理を妨げずに結果を得られます。
安全に使うコツ(初心者向け)
exceptを広げすぎない(Exceptionの丸取りは避ける)
なんでもexcept Exception
で受けると、想定外のバグまで隠してしまいます。
具体的な例外名で絞るのが基本です。
# 悪い例: 広く受けて握りつぶす -> 問題の特定が困難に
try:
1 / 0
except Exception:
pass # 何も出さないのは避ける
# 良い例: 具体的な例外だけを捕まえ、状況を明示する
try:
1 / 0
except ZeroDivisionError as e:
print(f"ゼロ除算でした: {e}")
ゼロ除算でした: division by zero
複数の想定があるならexcept (TypeError, ValueError) as e:
のようにタプルで列挙します。
エラーを握りつぶさない(原因を表示)
少なくとも「型名」と「メッセージ」を表示・記録しましょう。
これだけで原因追跡が大きく前進します。
# 型名とメッセージを出してから継続する
def to_int(s):
try:
return int(s)
except ValueError as e:
print(f"[変換失敗] {type(e).__name__}: {e} (入力: {s!r}) -> 0で代用")
return 0
print(to_int("42"))
print(to_int("abc")) # 失敗するが止まらない
42
[変換失敗] ValueError: invalid literal for int() with base 10: 'abc' (入力: 'abc') -> 0で代用
0
開発中は詳細を出し、本番では必要十分な情報だけに調整するのが現実的です。
例外オブジェクト(e)のメッセージを確認
例外オブジェクトe
には、原因を示す文字列や属性が入っています。
最低限、型名とメッセージを見ましょう。
try:
result = {}["missing"]
except KeyError as e:
print("型名:", type(e).__name__)
print("メッセージ:", str(e))
型名: KeyError
メッセージ: 'missing'
型名はtype(e).__name__
、テキストはstr(e)
で確認できます。
tryの範囲は最小にする
tryブロックは「落ちる可能性がある行」だけを囲み、前後の安全な処理は外に出します。
これにより、デバッグと保守が容易になります。
# 悪い例: まとめて囲んでしまう
try:
raw = "123"
n = int(raw) # ここが失敗しうる
doubled = n * 2
print(doubled)
except ValueError as e:
print("どこで失敗したか切り分けにくい:", e)
# 良い例: 失敗しうる箇所だけ最小で囲む
raw = "123"
try:
n = int(raw)
except ValueError as e:
print("数値化失敗:", e)
n = 0 # デフォルト
doubled = n * 2
print(doubled)
246
「最小で囲む」だけで、可読性も信頼性も大きく向上します。
継続のためのパターン
デフォルト値で処理を続ける
失敗時にデフォルト値へフォールバックして、全体の処理を継続するのは定番です。
# 文字列を数値に変換しつつ合計。失敗したら 0 と見なす
values = ["10", "20", "x", "30", "y"]
total = 0
for s in values:
try:
total += int(s)
except ValueError as e:
print(f"[注意] {type(e).__name__}: {e} -> '{s}' を 0 とみなして続行")
print("合計:", total)
[注意] ValueError: invalid literal for int() with base 10: 'x' -> 'x' を 0 とみなして続行
[注意] ValueError: invalid literal for int() with base 10: 'y' -> 'y' を 0 とみなして続行
合計: 60
入力の一部が壊れていても、全体の出力を得られるのがメリットです。
簡単なリトライ(回数を決める)
一時的な失敗(ネットワークなど)は一定回数のリトライで回避できます。
ここでは再現性のため、1回目は失敗、2回目で成功する関数を擬似的に作ります。
# 1回目は失敗、2回目で成功するダミー関数
attempt_state = {"count": 0}
def flaky_operation():
attempt_state["count"] += 1
if attempt_state["count"] < 2:
raise ConnectionError("一時的な接続エラー")
return "OK" # 2回目で成功
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
result = flaky_operation()
print(f"成功: {result} (試行{attempt}回目)")
break
except ConnectionError as e:
print(f"失敗(試行{attempt}回目): {e}")
if attempt == max_retries:
print("これ以上は試行せずに続行(または後で再実行)")
失敗(試行1回目): 一時的な接続エラー
成功: OK (試行2回目)
回数や待機時間(指数バックオフなど)は要件に応じて調整し、無限リトライは避けます。
失敗したデータをスキップして次へ進む
1件の失敗で全体を止めず、「失敗したものだけ」別扱いにして前へ進むのも有効です。
# 一部に不正データが混じる配列を処理し、失敗した項目は記録してスキップ
raw_items = ["100", "200", "bad", "300", None, "400"]
ok_items = []
skipped = []
for item in raw_items:
try:
ok_items.append(int(item))
except (ValueError, TypeError) as e:
print(f"[スキップ] {type(e).__name__}: {e} -> item={item!r}")
skipped.append(item)
print("処理できた項目:", ok_items)
print("スキップした項目:", skipped)
[スキップ] ValueError: invalid literal for int() with base 10: 'bad' -> item='bad'
[スキップ] TypeError: int() argument must be a string, a bytes-like object or a real number, not 'NoneType' -> item=None
処理できた項目: [100, 200, 300, 400]
スキップした項目: ['bad', None]
「エラーの個所だけ除外して前に進む」戦略は、データ処理の現場で非常に実用的です。
まとめ
try-exceptは「エラーが発生しても止まらずに処理を継続する」ための基本技術です。
重要なのは、
- 想定される例外を具体的に捕まえる
- 原因を記録して握りつぶさない
- tryの範囲を最小にする
- デフォルト値・スキップ・リトライなどの継続戦略を使い分ける
この4点です。
これらを徹底することで、プログラムは現実世界の不確実性に強くなります。
まずは本記事のサンプルを手元で動かし、「落ちないコード」の感覚を身につけてください。