【Python】DeprecationWarningを見逃さず、警告を必要な範囲だけ制御する

PythonのTopに戻る

将来の問題を警告の段階で見つける

DeprecationWarningは、今は動くが将来の版で変更・削除される機能を知らせるために使われる。通常の実行で見えない場合があるからといって、警告が存在しないとは限らない。テストでは必要な警告を表示または例外化し、原因を直すまでの間に限って、対象を絞った抑制を使う。

全警告をignoreにすると、別のライブラリや新しく発生した問題まで見えなくなる。以下では自作の小さな旧関数を使い、警告の記録、例外化、特定の警告だけの抑制をそれぞれ確認する。実ライブラリの設定を恒久的に書き換えることはせず、catch_warnings()の範囲内で切り替える。

警告を記録し、必要なら失敗に変える

example.py

import warnings


def old_scale(value):
    warnings.warn("old_scale is deprecated", DeprecationWarning, stacklevel=2)
    return value * 2


with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always", DeprecationWarning)
    assert old_scale(3) == 6
    assert len(caught) == 1
    assert caught[0].category is DeprecationWarning
    print(caught[0].category.__name__, str(caught[0].message))

with warnings.catch_warnings():
    warnings.simplefilter("error", DeprecationWarning)
    try:
        old_scale(3)
    except DeprecationWarning:
        print("promoted to exception")
    else:
        raise AssertionError("warning did not become an exception")

with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    warnings.filterwarnings(
        "ignore", message=r"^old_scale is deprecated$",
        category=DeprecationWarning,
    )
    assert old_scale(3) == 6
    warnings.warn("check calibration", UserWarning)
    assert len(caught) == 1
    assert caught[0].category is UserWarning
    print("still visible:", str(caught[0].message))

実行結果

DeprecationWarning old_scale is deprecated
promoted to exception
still visible: check calibration

警告の種類と処理方法は別

DeprecationWarningは警告の種類、alwaysやerrorはその警告をどう扱うかの指定である。alwaysにすると同じ警告の繰り返しも記録し、errorにするとその場所で例外として送出する。例外化したold_scale()では、警告の次のreturnへ進む前に止まるため、警告を表示するだけの場合と制御の流れも変わる。

record=Trueを付けると、表示する代わりに警告オブジェクトのリストへ記録できる。件数、category、messageを調べれば、期待する警告が出たかを自動で確認できる。出力の見た目だけを比較するより、警告の種類を直接検査する方がメッセージの表示書式に依存しにくい。

stacklevel=2は、この警告を出した関数そのものではなく、その呼び出し側を場所として示すための指定である。旧APIを使っている場所を見つけやすくなる。何段のラッパーを挟むかによって適切な値は変わるため、常に2を付ければ正しい位置になるわけではない。

抑制する範囲を狭く保つ

filterwarnings()には警告の種類とメッセージのパターンを渡した。^と$で例のメッセージ全体を対象にし、別のUserWarningが残ることも確認している。実際のライブラリでは必要に応じてmoduleなどでさらに絞れるが、stacklevelにより警告の発生位置が呼び出し側になる場合があるので、観測した情報を確認してから条件を決める。

catch_warnings()を抜けると、その範囲で変更したフィルターなどが元へ戻る。テストごとに設定を分離しやすい一方、CPython 3.12の通常の警告状態はプロセス全体に関わる。複数スレッドで同時にフィルターを変更する用途を、単にwithで囲んだから完全に分離されたと考えない。ここでは単一スレッドで実行している。

抑制は原因の修正ではない。何の旧機能を使っていて、どの版で対応する予定かを残し、ライブラリ更新時に不要になった設定を外す。広い条件のignoreが古いまま残ると、その後の別の警告を見落としやすくなる。

通常の実行で見えないとき

DeprecationWarningは、importしたライブラリ内で発生する場合には既定で隠れることがある。一方、__main__として実行するコードに帰属するものは表示対象になるなど、発生場所と既定フィルターによって結果が違う。警告が表示された一例から「常に見える」「常に隠れる」と一般化しない。

端末からはpython -Wdのような起動オプションで警告を見つける方法もある。継続的なテストでは、まず警告を一覧にして既知と新規を分け、対応可能な範囲から例外化すると導入しやすい。依存ライブラリの警告を一括で失敗にする場合は、更新しただけでテストが止まる可能性もあるので、運用の方針を明確にする。

警告を記録するテストでは、その警告が一度出たことだけで満足せず、今後のAPIへ移行した際に警告が出なくなることも確認する。警告の有無と関数の戻り値を両方確かめれば、表示を消すだけの変更で計算結果を壊していないかを調べられる。

実行環境と関連情報

掲載コードはLinux上のCPython 3.12.14で実行した。OS固有のファイル操作や対話環境の違いは、本文に記した条件に従って扱う。

関連:pytestで正常系と異常系の自動テストを始める / 別のPCで環境を再現する:依存関係の記録と入れ直し

PythonのTopに戻る