Python基礎 実案件/ポートフォリオ

Python実務の型:例外処理と構造化ログでエラーに強いコードを書く

例外処理って、結局どこまでやれば“実務で困らない”の?

ログも整えるのって大変そう…最低限の型、ください!

この記事は、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は即失敗
  • 上限は必須triescap)。ジッタでスパイク衝突を回避。

構造化ログ(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)
  • 相関IDcorr)で一意に追跡
  • 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標準ライブラリ
Python標準ライブラリ珠玉の10選|datetime・pathlib・itertoolsで実務が回るチートシート

外部ライブラリを増やさずに、コードの品質と保守性をグッと上げたい…。 標準ライブラリ“だけ”で、どこまで実務が回せる? 今回はそんな悩みを解決するために、Python標準ライブラリの“珠玉の10選”を ...

自動化
自動化:スケジューリングと業務改善の型|「再実行安全×観測可能×静かに動く」を仕組みにする

夜中に動かしているPython、自動で止まってた…ログもなくて原因が追えない…。 「毎朝のレポート」や「在庫監視」を、壊れず静かに回したい…! 業務で落ちない自動化を作る鍵は、(1) 再実行安全(Id ...

Python関数
Python関数とスコープの設計術:I/O分離×型ヒントで再利用性とテスト容易性を最大化

関数がどんどん太ってテストしづらい…どう整理すればいい? globalやprintが混ざっていて、ユニットテストを書く気力が出ない… この記事では、関数とスコープの設計を“純度×境界×型”の視点で整え ...

最近のコメント

    • この記事を書いた人
    • 最新記事

    ふみと

    このブログでは、データサイエンティストとして市場価値を上げる方法を独自にまとめて発信しています。

    【プロフィール】
    ・大手企業データサイエンティスト/マーケティングサイエンティスト(10年、年収900万円台)/案件100件以上
    ・資格:JDLA E資格(日本ディープラーニング協会主催)/JDLA Community(CDLE会員)/Advanced Marketer/ビジネス統計スペシャリスト/統計検定2級/TOEIC 805
    ・スキル:Python/Tableau/SQL/機械学習/Deep Learning/RPA

    -Python基礎, 実案件/ポートフォリオ