閉じる

Pythonのtry-except-finallyの正しい使い方(後始末/リソース解放)

実行中にエラーが起きても起きなくても、最後に必ずやっておきたい後始末(例: ファイルを閉じる、ロックを解放する)があります。

Pythonではfinallyを使うことで、この後始末を<strong>確実</strong>に実行できます。

本記事では初心者向けに、基本の書き方、実行順序、注意点、実例、そしてwith構文との使い分けまで丁寧に解説します。

finallyとは(Pythonのtry-except-finallyの基本)

finallyの役割: エラーの有無にかかわらず必ず実行

try-except-finallyは、エラー処理と後始末を分けて書ける構文です。

finallyエラーの有無にかかわらず必ず実行されるブロックで、典型的にはファイルを閉じる、データベース接続をクローズする、ロックを解放するなどのリソース解放に用います。

実行途中でreturnraiseがあってもfinallyは実行されるため、後始末を確実にできます。

実行順序(try → except → else → finally)

実行の流れは次のようになります。

最後に必ずfinallyが来る点が重要です。

  • まずtryの中を実行
  • 例外が発生したら対応するexcept
  • 例外が発生しなかった場合のみelse
  • 最後にfinallyを必ず実行

以下のコードで順序を確認できます。

Python
# 実行順序を出力して確認するサンプル
def flow(case: str) -> None:
    try:
        print("try: 開始")
        if case == "err":
            int("not-a-number")  # ValueErrorを発生させる
        else:
            print("try: 本処理を実行")
    except ValueError:
        print("except: ValueErrorを捕捉")
    else:
        print("else: 例外なしのときだけ実行")
    finally:
        print("finally: 後始末を必ず実行")
    print("関数末尾\n")

print("=== 正常系 ===")
flow("ok")

print("=== 異常系 ===")
flow("err")
実行結果
=== 正常系 ===
try: 開始
try: 本処理を実行
else: 例外なしのときだけ実行
finally: 後始末を必ず実行
関数末尾

=== 異常系 ===
try: 開始
except: ValueErrorを捕捉
finally: 後始末を必ず実行
関数末尾

try-finallyだけで使うケース

exceptで例外を処理せず、finallyだけで後始末するパターンがあります。

この場合、後始末だけは必ず行い、その後の例外処理は外側に任せたいときに有効です。

Python
# 例: 一時ファイルを確実に削除したいが、エラー処理自体は呼び出し側に任せる
import os
import tempfile

def do_something_with_temp() -> None:
    path = None
    try:
        tmp = tempfile.NamedTemporaryFile(delete=False)  # 自動削除を無効化
        path = tmp.name
        print(f"作成: {path}")
        tmp.write(b"hello")
        tmp.close()
        # ここで何かの処理... 例として故意にエラーを発生
        raise RuntimeError("途中で失敗しました")
    finally:
        # 例外の有無にかかわらず必ず削除する
        if path and os.path.exists(path):
            os.remove(path)
            print(f"削除: {path}")

try:
    do_something_with_temp()
except RuntimeError as e:
    print("外側で例外を捕捉:", e)
実行結果
作成: C:\Users\User\AppData\Local\Temp\tmpu37quewp
削除: C:\Users\User\AppData\Local\Temp\tmpu37quewp
外側で例外を捕捉: 途中で失敗しました

elseとの違い(参考)

else例外が発生しなかったときだけ実行されます。

対してfinally常に実行されます。

成功時のみ行いたい後処理(例: 成功メッセージの表示やコミット)はelse、成功・失敗に関わらず行いたい後始末(例: クローズ、ロック解放)はfinallyに置くのが原則です。

finallyの書き方と基本パターン

最小例(Pythonのfinallyの使い方)

最小の例では、tryで処理し、必要ならexceptで例外を処理し、最後に必ずfinallyで後始末します。

Python
def safe_div(a: float, b: float) -> float | None:
    try:
        print("割り算を実行します")
        result = a / b
        return result
    except ZeroDivisionError:
        print("0で割ることはできません")
        return None
    finally:
        # ここは必ず実行されます
        print("finally: 後始末(ここでは表示のみ)")

print("結果:", safe_div(10, 2))
print("結果:", safe_div(10, 0))
実行結果
割り算を実行します
finally: 後始末(ここでは表示のみ)
結果: 5.0
割り算を実行します
0で割ることはできません
finally: 後始末(ここでは表示のみ)
結果: None

エラーあり/なしの動きの違い

上の例のように、成功時はexceptを通らずにreturnしますが、それでも必ずfinallyが実行されます。

失敗時はexceptでエラー対応をした後、やはりfinallyが実行されます。

この「成功・失敗に依存せず最後に1回だけ」という性質のおかげで、後始末の重複や書き漏れを防げます。

returnやraiseがあってもfinallyは実行

returnraiseが途中にあってもfinallyは実行されます。

Python
def with_return() -> int:
    try:
        print("tryでreturnします")
        return 1
    finally:
        print("returnの前でもfinallyは実行されます")

def with_raise() -> None:
    try:
        print("tryで例外を送出します")
        raise ValueError("エラー発生")
    finally:
        print("raiseの前後でもfinallyは実行されます")

print("with_returnの戻り値:", with_return())

try:
    with_raise()
except ValueError as e:
    print("外側で捕捉:", e)
実行結果
tryでreturnします
returnの前でもfinallyは実行されます
with_returnの戻り値: 1
tryで例外を送出します
raiseの前後でもfinallyは実行されます
外側で捕捉: エラー発生

finally内の例外に注意

finally内で新たに例外を送出すると、元の例外が上書きされます

デバッグが難しくなるため、finallyでは極力例外を出さない設計にしましょう。

Python
# finally内の例外が元の例外を隠してしまう例
try:
    try:
        1 / 0  # ZeroDivisionError
    finally:
        raise KeyError("finallyで発生した別のエラー")
except Exception as e:
    print("最終的に表に出る例外:", type(e).__name__, e)
実行結果
最終的に表に出る例外: KeyError 'finallyで発生した別のエラー'

また、finallyでreturnすると、元のreturnや例外さえ上書きします

避けましょう。

Python
def bad_return() -> int:
    try:
        return 1
    finally:
        # これは「1」を上書きしてしまう
        return 2

print("戻り値:", bad_return())
実行結果
戻り値: 2

後始末とリソース解放の実例

ファイルを閉じる(open/close)

ファイル操作では確実にcloseすることが重要です。

ここではあえてwithを使わず、try-except-finallyでクローズします(後述のとおり実務ではwith推奨)。

Python
# ファイルを作成 -> 読み込み -> finallyでclose
def read_first_line(path: str) -> None:
    # デモ用にファイルを用意
    with open(path, "w", encoding="utf-8") as wf:
        wf.write("1行目\n2行目\n")

    f = None
    try:
        f = open(path, "r", encoding="utf-8")
        first = f.readline().rstrip("\n")
        print("読み込んだ1行目:", first)
    except FileNotFoundError:
        print("ファイルが見つかりません")
    finally:
        if f is not None:
            f.close()
            print("ファイルをcloseしました")

read_first_line("sample.txt")
実行結果
読み込んだ1行目: 1行目
ファイルをcloseしました

一時ファイルを削除する

一時ファイルは確実に削除したい代表例です。

Python
import os
import tempfile

def do_with_tmp() -> None:
    fd = None
    path = None
    try:
        fd, path = tempfile.mkstemp()  # 低レベルAPI: ファイル記述子とパスを返す
        print("作成:", path, "存在:", os.path.exists(path))
        os.write(fd, b"temp data")
        # ここで処理...
    finally:
        if fd is not None:
            os.close(fd)  # まずクローズ
        if path and os.path.exists(path):
            os.remove(path)  # その後削除
            print("削除:", path, "存在:", os.path.exists(path))

do_with_tmp()
実行結果
作成: /tmp/tmpxyz456 存在: True
削除: /tmp/tmpxyz456 存在: False

データベース接続をクローズ/ロールバック

成功時はコミット、失敗時はロールバック、最後にクローズという流れを明確にできます。

Python
import sqlite3

def db_transaction(success: bool) -> None:
    conn = sqlite3.connect(":memory:")  # デモではメモリDB
    try:
        cur = conn.cursor()
        cur.execute("CREATE TABLE t (id INTEGER)")
        cur.execute("INSERT INTO t VALUES (1)")
        if success:
            conn.commit()
            print("コミット完了")
        else:
            # 敢えてエラーを発生させる
            raise RuntimeError("DB処理に失敗")
    except Exception as e:
        print("例外発生:", e)
        conn.rollback()
        print("ロールバック完了")
    finally:
        conn.close()
        print("コネクションをcloseしました")

print("--- 成功時 ---")
db_transaction(True)
print("--- 失敗時 ---")
db_transaction(False)
実行結果
--- 成功時 ---
コミット完了
コネクションをcloseしました
--- 失敗時 ---
例外発生: DB処理に失敗
ロールバック完了
コネクションをcloseしました

ロックを確実に解放する

ロックは解放漏れがあるとデッドロックの原因になります。

必ずfinallyで解放しましょう。

Python
import threading

def use_lock(cause_error: bool) -> None:
    lock = threading.Lock()
    lock.acquire()
    try:
        print("ロック獲得中:", lock.locked())
        if cause_error:
            raise RuntimeError("クリティカルセクションで失敗")
        print("クリティカルセクションの処理")
    finally:
        if lock.locked():
            lock.release()
            print("ロックを解放しました")
    print("ロック状態(解放後):", lock.locked())

print("--- 正常 ---")
use_lock(False)
print("--- 例外発生 ---")
use_lock(True)
実行結果
--- 正常 ---
ロック獲得中: True
クリティカルセクションの処理
ロックを解放しました
ロック状態(解放後): False
--- 例外発生 ---
ロック獲得中: True
ロックを解放しました
ロック状態(解放後): False

ネットワーク接続をクローズする

ソケットも必ずcloseが必要です。

ネットワークの成否に関わらずfinallyでクローズします。

Python
import socket

def fetch_head_example() -> None:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(2)
    try:
        # example.com:80 へ接続を試みる
        result = s.connect_ex(("example.com", 80))
        if result == 0:
            print("接続成功")
            s.sendall(b"HEAD / HTTP/1.0\r\nHost: example.com\r\n\r\n")
            head = s.recv(64)
            print("1行目:", head.splitlines()[0].decode("ascii", "ignore"))
        else:
            print("接続失敗:", result)
    finally:
        s.close()
        print("ソケットをcloseしました")

fetch_head_example()
実行結果
接続成功
1行目: HTTP/1.0 200 OK
ソケットをcloseしました

(ネットワーク環境によっては「接続失敗: <エラーコード>」になることがあります。いずれの場合も最後のクローズは必ず行われます。)

with構文との使い分け

まずwith構文を優先(ファイル操作など)

リソースの多くは「コンテキストマネージャ」として実装されており、with構文で自動的に後始末できます。

可能なときはwithを優先しましょう。

Python
# withなら自動でcloseされる
path = "with_sample.txt"
with open(path, "w", encoding="utf-8") as w:
    w.write("hello with\n")

with open(path, "r", encoding="utf-8") as f:
    line = f.readline().strip()
    print("読み込み:", line)
# ここを出た時点でfは自動クローズ済み
print("自動クローズ確認:", f.closed)
実行結果
読み込み: hello with
自動クローズ確認: True

withが使えない場面でtry-finally

すべての操作がコンテキストマネージャ化されているわけではありません。

例えば「作業ディレクトリを一時的に変更して、必ず元に戻す」といった状態の復元にはtry-finallyが有効です。

Python
import os
import tempfile

def chdir_temporarily() -> None:
    original = os.getcwd()
    tmpdir = tempfile.mkdtemp()
    try:
        print("元のcwd:", original)
        os.chdir(tmpdir)
        print("変更後cwd:", os.getcwd())
        # ここでtmpdir内での処理を行う
    finally:
        # 例外の有無にかかわらず必ず元に戻す
        os.chdir(original)
        print("復帰cwd:", os.getcwd())

chdir_temporarily()
実行結果
元のcwd: /path/to/original
変更後cwd: /tmp/tmpabcd12
復帰cwd: /path/to/original

パターン比較(with vs try-except-finally)

以下は使い分けの目安です。

観点with構文try-except-finally
後始末の自動化自動手動で記述が必要
例外発生時のクリーンアップ自動で安全に実行書き方次第で安全に実行
可読性高い(ボイラープレートが少ない)やや冗長になりがち
複数リソースの管理withのネスト/併記で簡潔tryのネストが深くなりがち
状態の復元(os.chdirなど)適用困難なケースあり柔軟に記述可能

基本はwith、withにできない場面をtry-finallyで補う、という発想が安全です。

初心者向けベストプラクティス

初心者の方は、次を意識すると安全で読みやすいコードになります。

  • with構文を最優先し、対応していない場合のみtry-finallyを使う。
  • finallyでは例外を起こさない・returnしない(元の情報を上書きするため)。
  • finally短く・単純に(クローズやフラグ解除など最小限の後始末だけを書く)。
  • 成功時だけ行う処理(例: コミット、成功ログ)はelseに置く。
  • 例外を握りつぶさない。必要なら外側へ伝播させ、finallyで後始末だけ行う。

まとめ

finallyは成功・失敗に関わらず必ず実行されるため、後始末やリソース解放を確実に行う最重要の仕組みです。

実行順序はtry → except → else → finallyで、returnraiseがあってもfinallyは実行されます。

一方でfinally内の例外やreturnは元の情報を上書きするため避けるべきです。

実務ではまずwith構文を優先し、withで表現できない状態復元や特殊な後始末のみtry-finallyで補うのが安全な使い分けです。

これらの原則を押さえれば、ファイル、DB、ロック、ネットワークといった多様なリソースを堅牢かつ読みやすく扱えるようになります。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!