Python|try-except上級者向け設計ミスの見抜き方
この記事のポイント
- try-except構造における設計崩壊ポイントをレビューできる
 - 例外処理の粒度設計・責務分離ミスを発見する力が身につく
 - 例外漏洩、予期せぬ成功、誤捕捉パターンをコードから読み解く
 
そもそもtry-exceptとは
Pythonのtry-except構文は、例外発生時に処理を分岐させる仕組みです。以下のような基本形がよく使われます。
try:
    risky_operation()
except SomeException as e:
    handle_exception(e)これにより、エラー発生箇所とその処理を分けることができます。問題は、この便利さが過剰防御や設計ミスを招きやすいことです。
なぜこれをレビューするのか
実務でよく見るのは「とりあえずtry-exceptで囲っておく」コードです。これには以下の問題が内在します。
- エラー検出責務の混在
 - 正常系フローの隠蔽
 - 原因特定困難なcatch-all
 - 失敗時の状態不整合
 
レビューアーは「例外は制御フローの代替手段ではない」という原則を頭に置きつつ、try-exceptが設計意図通り機能しているかを読み取る必要があります。
レビューアー視点
- try-exceptで何の責務をカバーしているのか
 - 想定している例外種別は明示されているか
 - except節の中身が妥当か(ログ、リトライ、巻き戻し)
 - except節の粒度が適切か
 - except節に副作用が隠れていないか
 
開発者視点
- 例外種別を事前に調査して明示すること
 - 本来起きないはずの例外は潰さず上位に伝播させること
 - except節の副作用を安易に積み上げないこと
 - 例外をビジネスロジックの分岐代わりに使わないこと
 
良い実装例
なぜこの実装が良いのか
- 例外種別を
DatabaseErrorに限定しており、責務の範囲が明確 - 原因情報をロギングしており、障害調査に役立つ情報を確保
 - 例外を再送出することで上位層の障害監視が機能する
 - try-exceptの粒度が最小限で、巻き取りすぎていない
 - except節に副作用が存在せず、シンプルで読みやすい
 
# api_request_log_saver.py
import logging
from db_connection import insert_request_log, DatabaseError
class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        logging.error(f"DB error while saving request log: {e}")
        raise補足
再送出によって、障害監視・上位サービス層が異常検知できる設計になっています。ここでのexceptは責務範囲を超えていません。
レビュー観点
- 想定例外の明示有無
 - catch-allの使用有無(
except:,except Exception:) - except内での副作用実装有無
 - 状態巻き戻しや補正処理の要否検討
 - 例外情報の欠落(ログ記録に例外詳細含める)
 
良くない実装例: ケース1
典型的なtry-except乱用パターンを提示します。
# request_logger.py
import logging
from db_connection import insert_request_log
class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except:
@Reviewer例外種別が不明確です。最低限 `except Exception:` 等で限定してください。        logging.warning("Failed to save request log")
@Reviewer例外原因をログに出力していません。障害調査困難になります。例外内容を記録してください。@Reviewer例外を上位に再送出しておらず、異常検知ができなくなります。原則再送出してください。
問題点
- catch-allで何でも吸収してしまっている
 - 原因情報の欠落
 - 障害伝播阻害
 
改善例
# request_logger_improved.py
import logging
from db_connection import insert_request_log, DatabaseError
class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        logging.error(f"DB error: {e}")
        raise
    except Exception as e:
        logging.critical(f"Unexpected error while saving log: {e}")
        raise事前に発生可能性のあるDatabaseErrorは通常系。想定外のExceptionはクリティカル扱いし通知強度を上げています。責務分離が整理できている例です。
良くない実装例: ケース2(副作用込みパターン)
次にtry-except内で副作用を抱え込む典型例です。
# request_logger_with_fallback.py
import logging
from db_connection import insert_request_log
class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except:
@Reviewer例外種別が不明確です。副作用処理を伴う場合は特に例外種を限定してください。        logging.warning("Failed to save request log")
@Reviewerログ記録のみならず副作用が混在しています。責務分離を検討してください。        send_alert_email("DB failed, log saving skipped")
        retry_insert(log)
@Reviewerexceptブロックが肥大化しています。エラーハンドリング処理の分離が望ましいです。
問題点
- 副作用(通知・リトライ)とエラー処理が混在
 - 捕捉粒度が曖昧
 - 責務集中
 
改善例
# request_logger_separated.py
import logging
from db_connection import insert_request_log, DatabaseError
from notifications import send_alert_email
from retry import retry_insert
class ApiRequestLog:
    def __init__(self, request_id, endpoint, client_ip, response_code, requested_at):
        self.request_id = request_id
        self.endpoint = endpoint
        self.client_ip = client_ip
        self.response_code = response_code
        self.requested_at = requested_at
def save_request_log(log: ApiRequestLog):
    try:
        insert_request_log(log)
    except DatabaseError as e:
        handle_db_error(e, log)
def handle_db_error(e, log):
    logging.error(f"DB error while saving request log: {e}")
    send_alert_email("DB failed during request log save.")
    retry_insert(log)副作用ロジックはhandle_db_errorへ分離し、save_request_logの責務を単純化しています。レビューアー視点でも読みやすい構造です。
観点チェックリスト
まとめ
try-exceptはあくまで「設計の表現」であって「保険」ではありません。レビューアーは例外構造=設計意図の写像であると読み解き、単なる構文ミスではなく、責務崩壊を発見することが重要です。安易な捕捉や副作用の混入を許さず、責務を整理するレビュー技術を磨いていきましょう。