Pythonの例外処理とは、try の中で起きたエラー(例外)を except で受け止め、プログラムを止めずに適切に対処するための仕組みです。else は「例外が起きなかったとき」、finally は「例外が起きても起きなくても必ず」実行されます。

この記事では、基本の流れから、raise from で原因を引き継ぐ方法(__cause__ と __context__ の違い)、except: と except Exception: の違いを Ctrl+C を送って確かめた結果、Python 3.11からの ExceptionGroup・except*・add_note、Python 3.14で変わった点までを、すべて実際に動かした出力つきで解説します(Python 3.11.15、3.13.7、3.14.0rc2で確認)。

Pythonの例外処理 早見表

やりたいこと 書き方
例外を受け止める try: ... except ValueError: ...
例外の中身を使う except ValueError as e:
複数の例外を同じように処理 except (ValueError, TypeError):(3.14からは括弧なしも可)
成功したときだけ実行 else:
必ず後片付けする finally: または with 文
例外を投げる raise ValueError("メッセージ")
受け止めた例外をそのまま投げ直す raise(引数なし)
別の例外に変換し、原因を残す raise AppError(...) from e
原因を表示しない raise AppError(...) from None
独自の例外を作る class AppError(Exception): ...
複数の例外をまとめて投げる・受ける ExceptionGroup と except*(3.11+)
例外に補足情報を足す e.add_note("...")(3.11+)
スタックトレースつきでログに出す logging.exception("...")
特定の例外を意図して無視する with contextlib.suppress(FileNotFoundError):

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python例外処理 try exceptの書き方|raise fromも実行で確認」

try・except・else・finally の流れ

def run(x):
    print(f"--- run({x!r})")
    try:
        print("try: 開始")
        n = 10 / int(x)
    except ValueError as e:
        print(f"except ValueError: {e}")
    except ZeroDivisionError as e:
        print(f"except ZeroDivisionError: {e}")
    else:
        print(f"else: 結果={n}")
    finally:
        print("finally: 必ず実行")

run("2"); run("abc"); run("0")
--- run('2')
try: 開始
else: 結果=5.0
finally: 必ず実行
--- run('abc')
try: 開始
except ValueError: invalid literal for int() with base 10: 'abc'
finally: 必ず実行
--- run('0')
try: 開始
except ZeroDivisionError: division by zero
finally: 必ず実行
節 実行されるとき 主な用途
try 最初に必ず 例外が起きるかもしれない処理(なるべく短く)
except 対応する例外が起きたとき 回復、変換、ログ
else 例外が起きなかったとき 成功したときの続きの処理
finally 常に(returnの前にも) 後片付け(ファイルを閉じる等)

else を使うと、try の範囲を「本当に例外を想定している行」だけに絞れます。try の中に成功後の処理まで入れると、その処理で起きた想定外の例外まで同じ except で受け止めてしまいます。

finally は return の前にも実行される

def f():
    try:
        return "try"
    finally:
        print("finally runs before return")

print(f())
finally runs before return
try

複数の例外と as e

まとめて受け止める

for v in ["x", None]:
    try:
        int(v)
    except (ValueError, TypeError) as e:
        print(type(e).__name__, ":", e)
ValueError : invalid literal for int() with base 10: 'x'
TypeError : int() argument must be a string, a bytes-like object or a real number, not 'NoneType'

Python 3.14からは、as を使わない場合に限り、括弧なしでも書けます(PEP 758)。

try:
    int(None)
except ValueError, TypeError:
    print("3.14: 括弧なしで複数の例外を捕まえた")
Python 3.13:SyntaxError: multiple exception types must be parenthesized
Python 3.14:3.14: 括弧なしで複数の例外を捕まえた

複数のバージョンで動かすコードでは、括弧つきで書いておくと安心です。

except は上から順に1つだけ選ばれる

try:
    int("x")
except Exception:
    print("Exceptionで捕まった")
except ValueError:
    print("ValueErrorで捕まった")
Exceptionで捕まった

ValueError は Exception の子クラスなので、先に書いた except Exception に一致します。具体的な例外を上に、広い例外を下に書きます。

as e の変数は except を抜けると消える

try:
    1 / 0
except ZeroDivisionError as e:
    pass
print(e)
NameError: name 'e' is not defined

as e の変数は、except ブロックの終わりで削除されます。後で使いたいときは、ブロック内で別の変数に代入しておきます。

raise と raise from:原因を引き継ぐ

低いレベルの例外(ValueError など)を、アプリの意味のある例外に変換する場面はよくあります。3つの書き方で結果を比べます(A〜Cをそれぞれ関数 load_a〜load_c の中に書いて実行しました)。

class ConfigError(Exception):
    pass

# A: raise from e
try:
    int("abc")
except ValueError as e:
    raise ConfigError("port の設定が不正です") from e

# B: from なし
except ValueError:
    raise ConfigError("port の設定が不正です")

# C: from None
except ValueError:
    raise ConfigError("port の設定が不正です") from None

A:raise ... from e の出力

Traceback (most recent call last):
  File "ex2.py", line 6, in load_a
    int("abc")
ValueError: invalid literal for int() with base 10: 'abc'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  ...
ConfigError: port の設定が不正です

B:from なしの出力

ValueError: invalid literal for int() with base 10: 'abc'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  ...
ConfigError: port の設定が不正です

C:from None の出力

Traceback (most recent call last):
  ...
ConfigError: port の設定が不正です

受け止めた例外の属性を調べた結果です。

load_a cause= ValueError("invalid literal ...") context= ValueError("invalid literal ...") suppress= True
load_b cause= None context= ValueError("invalid literal ...") suppress= False
load_c cause= None context= ValueError("invalid literal ...") suppress= True
書き方 __cause__ __context__ 表示される文 意味
raise X from e e e The above exception was the direct cause... 「eが原因で X にした」と意図を明示
raise X(except内) None e(自動) During handling of the above exception... 「処理中に別の例外が起きた」(意図が伝わりにくい)
raise X from None None e(残るが非表示) 元の例外は表示されない 元の例外を意図して隠す

__context__ は自動で記録され、__cause__ は from で明示したときだけ設定されます。例外を変換するときは from e を付けるのが基本です。ログを読む人に「変換した結果である」ことがはっきり伝わります。from None は、元の例外が利用者にとって意味のないノイズになる場合に限って使います。

投げ直しは raise だけでよい

受け止めた例外をそのまま上に伝えるときは、引数なしの raise を使います。元のスタックトレースがそのまま残ります。

例外の握りつぶしと、except: と except Exception: の違い

握りつぶすと、誤りに気づけない

def parse_all(values):
    total = 0
    for v in values:
        try:
            total += int(v)
        except:
            pass
    return total

print(parse_all(["1", "2", "x", None, "4"]))
7

"x" と None が黙って飛ばされ、何件失敗したのか誰にもわかりません。想定している例外だけを受け止め、失敗を記録するように直します。

def parse_all2(values):
    total, errors = 0, []
    for v in values:
        try:
            total += int(v)
        except (ValueError, TypeError) as e:
            errors.append((v, type(e).__name__))
    return total, errors
(7, [('x', 'ValueError'), (None, 'TypeError')])

except: は Ctrl+C まで受け止めてしまう

自分自身に SIGINT(Ctrl+C と同じシグナル)を送り、さらに sys.exit(1) を呼ぶコードで比べました。

def work():
    os.kill(os.getpid(), signal.SIGINT)  # Ctrl+C を押したのと同じ
    time.sleep(0.1)

def exit_work():
    sys.exit(1)

except: の場合

work: except: で捕まえた(処理を続行)
exit_work: except: で捕まえた(処理を続行)
最後まで実行された
exit=0

except Exception: の場合

Traceback (most recent call last):
  ...
KeyboardInterrupt
exit=130

except: は Ctrl+C でも sys.exit() でも止まらず、終了コードも0(成功)になりました。except Exception: なら、Ctrl+C でプログラムが正しく止まります。

KeyboardInterrupt の親: (KeyboardInterrupt, BaseException, object)
SystemExit の親       : (SystemExit, BaseException, object)
書き方 受け止める範囲 Ctrl+C・sys.exit()
except ValueError: その例外と子クラスだけ 止まる
except Exception: 通常の例外すべて 止まる
except BaseException: / except: すべて 止まらない

KeyboardInterrupt と SystemExit は Exception ではなく BaseException を直接継承しています。except Exception: は「最後の受け皿」として使い、except: は避けるのが基本です。

意図して無視するなら suppress

from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove("no_such_file.tmp")

「ファイルがなければ何もしない」のように無視することが仕様なら、suppress で対象を限定して書くと、意図がコードから読み取れます。

独自の例外クラスを作る

class AppError(Exception):
    """アプリ共通の基底例外"""

class NotFoundError(AppError):
    def __init__(self, resource, key):
        super().__init__(f"{resource} が見つかりません: {key!r}")
        self.resource, self.key = resource, key

class ValidationError(AppError):
    pass

try:
    raise NotFoundError("user", 42)
except AppError as e:
    print(type(e).__name__, ":", e, "| key =", e.key)
NotFoundError : user が見つかりません: 42 | key = 42
ポイント 内容
Exception を継承 BaseException は継承しない
アプリ共通の基底例外を作る except AppError でアプリの例外だけをまとめて受け止められる
名前は 〜Error 標準の例外と同じ命名
調査に必要な値を属性に持つ key などをメッセージと属性の両方に

標準の例外で意味が足りるとき(引数の値が不正 → ValueError、型が違う → TypeError)は、標準の例外を使う方がわかりやすくなります。

with 文で後片付けを確実にする

class Conn:
    def __enter__(self):
        print("open"); return self
    def __exit__(self, exc_type, exc, tb):
        print(f"close (exc_type={exc_type.__name__ if exc_type else None})")
        return False   # 例外を握りつぶさない

try:
    with Conn():
        raise RuntimeError("処理中のエラー")
except RuntimeError as e:
    print("caught:", e)
open
close (exc_type=RuntimeError)
caught: 処理中のエラー

with 文は、例外が起きても __exit__ で必ず後片付けをします。ファイル(open)、ロック、DB接続などは try/finally を書くより with を使うのが基本です。__exit__ が True を返すと例外が消えるので、通常は False(または何も返さない)にします。

ExceptionGroup と except*(3.11+)

入力チェックのように、複数のエラーをまとめて返したいときに使います。

def validate(user):
    errors = []
    if not user.get("name"):
        errors.append(ValueError("name は必須です"))
    if not isinstance(user.get("age"), int):
        errors.append(TypeError("age は整数で指定してください"))
    if "@" not in user.get("email", ""):
        errors.append(ValueError("email の形式が不正です"))
    if errors:
        raise ExceptionGroup("入力エラー", errors)

try:
    validate({"name": "", "age": "20", "email": "x"})
except* ValueError as eg:
    for e in eg.exceptions:
        print("ValueError:", e)
except* TypeError as eg:
    for e in eg.exceptions:
        print("TypeError :", e)
ValueError: name は必須です
ValueError: email の形式が不正です
TypeError : age は整数で指定してください

except* は、グループの中から一致する例外だけを取り出します。普通の except と違い、一致する except* がすべて実行されます。

受け止めなかった場合の表示です。

  + Exception Group Traceback (most recent call last):
  |   File "ex7.py", line 1, in <module>
  |     raise ExceptionGroup("入力エラー", [...])
  | ExceptionGroup: 入力エラー (2 sub-exceptions)
  +-+---------------- 1 ----------------
    | ValueError: name は必須です
    +---------------- 2 ----------------
    | TypeError: age は整数で指定してください
    +------------------------------------

普通の except ValueError: では、ExceptionGroupの中の ValueError は受け止められません。

ValueError では捕まらず、ExceptionGroup で捕まった: g (1 sub-exception)

asyncio.TaskGroup での例

async def fetch(n):
    await asyncio.sleep(0.01 * n)
    if n % 2 == 0:
        raise ConnectionError(f"task{n} 接続失敗")
    return n

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            for n in range(1, 5):
                tg.create_task(fetch(n))
    except* ConnectionError as eg:
        print(len(eg.exceptions), "件失敗:", [str(e) for e in eg.exceptions])
1 件失敗: ['task2 接続失敗']

task4も失敗するはずですが、1件目の失敗(task2)で残りのタスクがキャンセルされたため、1件だけでした。TaskGroupの失敗は ExceptionGroup で届くので、except* で受け止めます。

add_note で補足情報を足す(3.11+)

def import_rows(rows):
    for i, row in enumerate(rows, 1):
        try:
            int(row["qty"])
        except ValueError as e:
            e.add_note(f"{i}行目: row={row}")
            raise

import_rows([{"qty": "1"}, {"qty": "abc"}])
Traceback (most recent call last):
  File "ex5.py", line 8, in <module>
    import_rows([{"qty": "1"}, {"qty": "abc"}])
  File "ex5.py", line 4, in import_rows
    int(row["qty"])
ValueError: invalid literal for int() with base 10: 'abc'
2行目: row={'qty': 'abc'}

例外の種類を変えずに「何行目で、どの値で」を足せます。メモは e.__notes__ にリストで入ります。別の例外に変換するほどではないときに便利です。

logging.exception でスタックトレースを残す

import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger("app")

try:
    {"a": 1}["b"]
except KeyError:
    log.exception("設定の読み込みに失敗しました")
ERROR app: 設定の読み込みに失敗しました
Traceback (most recent call last):
  File "ex4.py", line 43, in <module>
    {"a": 1}["b"]
    ~~~~~~~~^^^^^
KeyError: 'b'

log.exception() は ERROR レベルで、スタックトレースも自動で出力します。except の中で使います。print(e) では 'b' としか出ず、どこで起きたかがわかりません。

ログに出したうえで raise し、呼び出し元でもログに出すと、同じエラーが何度も記録されます。「ここで対処を終えるならログ」「上に伝えるならログは最後に受け止めた場所で1回」と決めておくと、ログが読みやすくなります。

EAFP と LBYL

cfg = {"host": "localhost"}

# LBYL:先に確認してから使う
if "port" in cfg:
    port = cfg["port"]
else:
    port = 80

# EAFP:まず使い、例外が起きたら対処する
try:
    port = cfg["port"]
except KeyError:
    port = 80

# 用意されている書き方があれば、それが一番簡潔
port = cfg.get("port", 80)

3つとも結果は 80 です。

スタイル 意味 向いている場面
EAFP(Easier to Ask Forgiveness than Permission) まず実行し、失敗したら例外で対処 ファイルやネットワークなど、確認と使用の間に状態が変わりうるもの
LBYL(Look Before You Leap) 先に条件を確認する 失敗が頻繁に起きる、条件が単純

Pythonの公式用語集ではEAFPが「Pythonでよく見られるスタイル」として紹介されています。ファイルの存在を os.path.exists() で確認してから開くと、確認と open の間に削除される可能性が残るため、try: open(...) except FileNotFoundError: の方が確実です。一方で dict.get() のように専用の書き方があるときはそれを使うのが一番読みやすくなります。

Python 3.14 での変更点

変更 内容 確認結果
PEP 758 as なしなら except A, B: と括弧なしで書ける 3.13はSyntaxError、3.14は実行できた
PEP 765 finally の中の return・break・continue に警告 下記のとおり
def calc():
    try:
        raise RuntimeError("本当のエラー")
    finally:
        return -1

print("result =", calc())
Python 3.13:result = -1
Python 3.14:ex9.py:5: SyntaxWarning: 'return' in a 'finally' block
             result = -1

finally の中で return すると、try で起きた例外が消えて -1 が返ります。3.14からは警告で気づけるようになりました。finally は後片付けだけに使います。

よくある失敗と直し方

よくある書き方 起きること 直し方
except: pass すべての失敗が見えなくなる。Ctrl+Cも効かない 想定する例外だけ受け止め、記録する
try の範囲が広い 想定外の行の例外まで受け止める try を短くし、続きは else へ
変換時に from を付けない 「処理中に別の例外」と表示され、意図が伝わらない raise X(...) from e
print(e) だけ どこで起きたかわからない log.exception(...)
except の中で同じ処理を再実行 同じ原因でまた失敗する 原因ごとに対処を分ける。再試行は回数と間隔を決めて行う
finally で return 例外が消える finally は後片付けだけ
raise e で投げ直す 投げ直した行がトレースに加わる 引数なしの raise

よくある質問

Q. except Exception as e と書けば安全ですか? A. プログラムの一番外側(リクエスト単位やバッチ全体)で、ログに出して処理を終える「最後の受け皿」としては有効です。途中の関数で使うと想定外の誤りまで受け止めてしまうので、途中では具体的な例外を指定します。

Q. else を使わずに try の最後に書くのと、何が違いますか? A. try の最後に書くと、その処理で起きた例外も同じ except に入ります。else に書くと、その処理の例外は except に入らず、そのまま上に伝わります。「どの行の失敗を想定しているか」がはっきりします。

Q. 独自例外は Exception と BaseException のどちらを継承しますか? A. Exception を継承します。BaseException を直接継承すると、except Exception: で受け止められなくなり、Ctrl+C と同じ扱いになってしまいます。

Q. ExceptionGroup は普段のコードでも使いますか? A. 主に複数のエラーをまとめて報告したいとき(入力チェック、並行処理)に使います。asyncio.TaskGroup を使う場合は失敗が ExceptionGroup で届くため、except* の書き方を知っておくと役立ちます。

まとめ

  • try → 失敗なら except、成功なら else、どちらでも finally の順に実行される
  • except は上から1つだけ選ばれる。具体的な例外を上に書く
  • 例外を変換するときは raise ... from e。__cause__ に原因が残り、意図が伝わる
  • except: は Ctrl+C や sys.exit() まで受け止める。最後の受け皿は except Exception:
  • 独自例外は Exception を継承し、アプリ共通の基底例外を作る
  • 3.11+ は ExceptionGroup・except*・add_note、3.14 は括弧なしの複数指定と finally 内 return の警告
  • ログは logging.exception() で、スタックトレースを1回だけ残す

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python例外処理 try exceptの書き方|raise fromも実行で確認」

株式会社RJCでは、エラーが起きたときに原因をすばやく追えるコードの書き方を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?