
例外処理って、結局どこまでやれば“実務で困らない”の?
ログも整えるのって大変そう…最低限の型、ください!
この記事は、pythonbunseki.comの実務トーンで「防ぐ→気づく→復旧する」をコードに落とし込み、例外処理とログ設計を“そのまま貼って使える”形でまとめたものです。ふみとが現場で使っている例外の設計指針、指数バックオフ+ジッタ、構造化(JSON)ログ、相関ID、通知、pytestでの再現を、標準ライブラリ中心で実装していきます。
この記事で身に付く力
- 例外境界の設計力(I/O・外部依存・データ整合性・業務ルール)
- 構造化ログ(JSON+相関ID+レベル運用)
- 堅牢な再試行(指数バックオフ+ジッタ+上限)
- 失敗の再現性(pytestの最小セット)
- フェイルセーフ/通知(止めない・気づける運用)
リード(結論)
実務で強いPythonコードは、最初から例外とログが入っています。鍵は、①防ぐ(設計):境界ごとにtry/except/else/finally
を置く、②気づく(観測):JSONログ+相関ID+レベル運用で「どこで落ちたか」を1秒で掴む、③復旧する(運用):指数バックオフとフェイルセーフを型にする、の3点です。
関連記事:
>>Python関数とスコープの設計術:I/O分離×型ヒントで再利用性とテスト容易性を最大化
>>『読める・速い・止まる』Pythonの制御構文|if / for / whileの設計術
>>もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ
>>【コピペOK】pytestで“壊れないPython”を作る12ステップ
>>作業時間を半減する環境構築:VSCode/タスクランナー|“保存で整う・ワンキーで回る”仕組み化テンプレ
>>Webスクレイピング:requests×BeautifulSoupの基本|“合法×丁寧×再現性”でデータ取得を設計する
まず押さえる:現場で起きがちな3つの事故
- 沈黙クラッシュ:例外が握りつぶされ、誰も気づかない。
- print地獄:時刻/レベルなしのログが散在し、追跡不能。
- リトライ暴走:無限再試行で相手先に迷惑、重複実行でデータ汚染。
体験談:某ECの夜間バッチで、レスポンス遅延に対して無限リトライが発動。朝まで同じ注文が5回実行され、翌朝の電話が鳴り止まなかったことが…。最初から上限とジッタを入れておけば防げます。
設計の全体像:「防ぐ→気づく→復旧する」
- 防ぐ:I/O、外部依存、パース/検証、業務ルールの4つの境界に薄く
try
を置く。 - 気づく:JSON Lines形式のログ+相関IDで1リクエストを追えるように。
- 復旧する:指数バックオフ+上限、フェイルセーフ、通知のスロットリング。
例外の設計指針:自分の例外に包む(ラップ)
class AppError(Exception):
"""アプリ共通の基底例外(捕捉はこれ1つでOK)"""
class ExternalError(AppError):
pass # API/ネット/DB 由来
class DataError(AppError):
pass # パース/検証 由来
class BusinessRuleError(AppError):
pass # 業務ルール違反
原則:標準例外をそのまま遠くまで投げず、境界で自分の例外に包んで情報を増やして再送(raise ... from e
)します。
from pathlib import Path
import json
def load\_config(path: Path) -> dict:
try:
text = path.read\_text(encoding="utf-8")
return json.loads(text)
except FileNotFoundError as e:
raise ExternalError(f"config not found: {path}") from e
except json.JSONDecodeError as e:
raise DataError(f"invalid json: {path}: {e}") from e
try/except/else/finally
は薄く広く配置し、巨大なtry
で一気に包まないのがコツです。
f = open("data.csv", "r", encoding="utf-8")
try:
rows = f.readlines() # 例外が出得る本処理
except OSError as e:
... # ログ&代替
else:
... # 成功時だけ実行
finally:
f.close() # 成否に関わらず必ず実行
# 実務では with 文が推奨
指数バックオフ+ジッタ:標準ライブラリだけで堅牢に
import time, random, urllib.request, logging
log = logging.getLogger(**name**)
def get\_with\_retry(url: str, \*, tries: int = 5, base: float = 0.5, cap: float = 8.0) -> str:
"""指数バックオフ+フルジッタ。最大 tries で中断。"""
for attempt in range(1, tries + 1):
try:
with urllib.request.urlopen(url, timeout=5) as r:
if r.status >= 500:
raise ExternalError(f"server {r.status}")
return r.read().decode()
except (TimeoutError, OSError) as e:
\# リトライ対象:ネット系の一過性エラー
if attempt == tries:
raise ExternalError(f"retry exceeded: {url}") from e
sleep = min(cap, base \* 2 \*\* (attempt - 1)) \* random.random()
log.warning("retry %s/%s in %.2fs: %s", attempt, tries, sleep, e)
time.sleep(sleep)
- 対象例外を限定(一過性のネット障害など)。4xxは即失敗。
- 上限は必須(
tries
とcap
)。ジッタでスパイク衝突を回避。
構造化ログ(JSON)と相関IDで“追える”ログに
import logging, logging.config, json
from logging.handlers import TimedRotatingFileHandler
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S%z"),
"lvl": record.levelname,
"msg": record.getMessage(),
"logger": record.name,
"module": record.module,
"line": record.lineno,
"corr": getattr(record, "corr", "-"),
}
if record.exc\_info:
payload\["exc"] = self.formatException(record.exc\_info)
return json.dumps(payload, ensure\_ascii=False)
handler = TimedRotatingFileHandler("app.log", when="midnight", backupCount=7, encoding="utf-8")
handler.setFormatter(JsonFormatter())
root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(handler)
import logging, uuid, contextvars
corr\_var: contextvars.ContextVar\[str] = contextvars.ContextVar("corr", default="-")
class CorrAdapter(logging.LoggerAdapter):
def process(self, msg, kwargs):
extra = kwargs.setdefault("extra", {})
extra\["corr"] = corr\_var.get()
return msg, kwargs
base = logging.getLogger(**name**)
log = CorrAdapter(base, {})
def with\_corr(func):
def wrapper(\*a, \*\*kw):
token = corr\_var.set(uuid.uuid4().hex\[:12])
try:
return func(\*a, \*\*kw)
finally:
corr\_var.reset(token)
return wrapper
@with\_corr
def run\_job():
log.info("start job")
try:
...
except AppError:
log.exception("failed") # exc\_info 付き
raise
finally:
log.info("end job")
ログのレベル運用(実務の型)
- DEBUG:開発者向けの詳細(本番は最小限)
- INFO:開始/終了・件数・所要などのサマリ
- WARNING:自動復旧した異常(リトライ/フォールバック)
- ERROR:要調査の失敗(ビジネス影響)
- CRITICAL:停止級。即時通知対象
通知とフェイルセーフ:“止めない・鳴らしすぎない”
- 通知:ERROR/CRITICALをメール/Slackへ。スロットリング(例:1分1回)で多重通知を防止。
- フェイルセーフ:取得失敗→キャッシュ/前回値。レポート生成失敗→空テンプレ+注記で納品は止めない。スクレイピングは規約/robots順守、429/403は停止。→ [内部リンク:Webスクレイピングの法的リスクと安全運用]
「握り方」の良し悪し:ログ+代替が基本
悪い例:何もせず無視(最悪)
try:
risky()
except Exception:
pass # 何もせず無視
良い例:構造化ログ+代替 or 再送。握らないなら上位へraise
。
try:
risky()
except DataError as e:
log.warning("invalid data: %s", e, exc_info=True)
return Fallback()
失敗を“再現”する:pytest最小セット
# app.py
class AppError(Exception): ...
class DataError(AppError): ...
def parse\_int(s: str) -> int:
try:
return int(s)
except ValueError as e:
raise DataError(f"not int: {s}") from e
# tests/test_app.py
import pytest
from app import parse_int, DataError
def test\_parse\_int\_ok():
assert parse\_int("42") == 42
def test\_parse\_int\_ng():
with pytest.raises(DataError) as ei:
parse\_int("xx")
assert "not int" in str(ei.value)
監査と運用で守る“ログの型”
- タイムスタンプはISO8601(例:2025-09-05T09:30:00+09:00)
- 相関ID(
corr
)で一意に追跡 - Who/What/Resultを必ず記録(誰が何をして結果は?)
- PII禁止(個人情報・鍵は出さない)
- ローテーション(日次/7日)+必要に応じ保全
今日やること(45分)
- 基底例外
AppError
と分類例外(External/Data/Business)を導入 - JSONログ+相関ID(LoggerAdapter or contextvars)を設定
- 指数バックオフ関数でHTTP呼び出しをラップ
- pytestで失敗パターンを2本“再現”
体験談:副業で納品したレポート生成バッチは、データ供給元が時々落ちます。前回成功時のキャッシュと空テンプレ納品+注記を入れておくと、納品を止めずに原因切り分けもスムーズになりました。
まとめ
本記事では例外処理とログ設計の型を、コピペで導入できるレベルまで分解しました。最初から防ぐ→気づく→復旧するを組み込むと、チームの調査時間は激減し、信頼性は大幅に向上します。まずは「今日やること」の4項目から着手してみてください。
-
-
【コピペOK】pytestで“壊れないPython”を作る12ステップ
「昨日は動いてたのに、今日は壊れた…」 データ分析やETL、機械学習のコードで多発するこの悲劇。実は“テスト不在”が9割です。 本記事は、pytestで“壊れないPython”を作るための実務ガイド。 ...
-
-
もう事故らせない:PythonでCSV/JSON/Excelを安全に読み書きする実務レシピ
CSV/JSON/Excelの読み書き、どこから気をつければいい? 文字化け・先頭ゼロ欠落・壊れたExcel……もう事故らせたくない! 結論:データ仕事の9割はI/O(入出力)。ここを整えるだけで、桁 ...
-
-
Python標準ライブラリ珠玉の10選|datetime・pathlib・itertoolsで実務が回るチートシート
外部ライブラリを増やさずに、コードの品質と保守性をグッと上げたい…。 標準ライブラリ“だけ”で、どこまで実務が回せる? 今回はそんな悩みを解決するために、Python標準ライブラリの“珠玉の10選”を ...
-
-
自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする
夜中に動かしているPython、自動で止まってた…ログもなくて原因が追えない…。 「毎朝のレポート」や「在庫監視」を、壊れず静かに回したい…! 業務で落ちない自動化を作る鍵は、(1) 再実行安全(Id ...
-
-
Python関数とスコープの設計術:I/O分離×型ヒントで再利用性とテスト容易性を最大化
関数がどんどん太ってテストしづらい…どう整理すればいい? globalやprintが混ざっていて、ユニットテストを書く気力が出ない… この記事では、関数とスコープの設計を“純度×境界×型”の視点で整え ...
最近のコメント