プログラムは最後まで動いたのに、画面に見慣れない英語のメッセージが出ていた。そんな経験はありませんか。よく見るとDeprecationWarningやUserWarningと書いてあって、エラーなのかどうかもわからない。
赤い文字で何か出ていると、それだけで少し不安になりますよね。けれども、慌てて検索する前に知っておきたいことがあります。
これはエラーではなく、警告です。Pythonには、処理を止めずに注意だけを伝えるための仕組みが用意されています。
今回は、その仕組みを担う標準ライブラリのwarningsについて、警告の読み方から、自分で警告を出す方法、消し方、エラーに変える方法まで順番に見ていきます。
警告とエラーは何が違うのか¶
まずは、エラーと警告の違いをはっきりさせておきましょう。ここがわかると、画面に何か出たときの落ち着き方がまったく変わります。
エラー、つまり例外が起きると、Pythonはその場で処理を止めます。try文で受け止めない限り、プログラムはそこで終わりです。
一方で警告は、メッセージを表示するだけで、処理はそのまま続きます。いまは動くけれど、このままだと将来困るかもしれない、という種類の知らせです。
違いを表にまとめると、次のようになります。
| 項目 | エラー(例外) | 警告 |
|---|---|---|
| 処理 | その場で止まる | そのまま続く |
| 主な意味 | いま動かない | いまは動くが注意が必要 |
| 出し方 | raise |
warnings.warn() |
| 受け止め方 | try / except |
フィルターで表示・非表示・エラー化を選ぶ |
| 同じ場所で何度も起きたとき | 毎回起きる | 初期設定では最初の1回だけ表示 |
最後の行は、意外と知られていないポイントです。ループの中で同じ警告が出ても、初期設定では1回しか表示されません。
例外の考え方そのものに自信がない方は、先にこちらを読んでおくと理解しやすくなります。【関連記事】やってはいけない例外処理
警告メッセージの読み方¶
では、実際の警告メッセージを見てみましょう。読み方さえわかれば、どこを直せばいいかはすぐに見えてきます。
次のような短いスクリプトを、w.pyという名前で保存して実行してみます。
import warnings
def old_func():
warnings.warn(
"old_funcは非推奨です。new_funcを使ってください",
DeprecationWarning,
stacklevel=2,
)
return 1
def load(path):
warnings.warn("設定ファイルが見つからないので既定値を使います", UserWarning)
return {}
old_func()
load("config.toml")
print("done")
手元のPython 3.11で実行すると、次のように表示されました。表示されるパスは、実行した場所によって変わります。
w.py:15: DeprecationWarning: old_funcは非推奨です。new_funcを使ってください
old_func()
w.py:12: UserWarning: 設定ファイルが見つからないので既定値を使います
warnings.warn("設定ファイルが見つからないので既定値を使います", UserWarning)
done
1行目は、ファイル名、行番号、警告の種類、メッセージの順に並んでいます。2行目には、その行のコードがそのまま表示されます。
そして最後にdoneが出ているので、処理は最後まで進んでいます。警告が出ても止まらない、というのがよくわかりますね。
警告にも種類がある¶
警告には、目的ごとにいくつかの種類があります。カテゴリと呼ばれ、どれもWarningというクラスを親に持っています。
よく見かけるものを表にまとめました。
| カテゴリ | 主な意味 |
|---|---|
UserWarning |
種類を指定しなかったときの既定。一般的な注意 |
DeprecationWarning |
非推奨。将来のバージョンで削除や変更の予定がある |
PendingDeprecationWarning |
まだ先だが、いずれ非推奨になる予定 |
FutureWarning |
将来、動きが変わる予定。利用者向けに表示される |
RuntimeWarning |
実行時の怪しい動き |
SyntaxWarning |
文法としては通るが怪しい書き方 |
ResourceWarning |
ファイルなどの閉じ忘れ |
初心者がいちばんよく出会うのは、DeprecationWarningとFutureWarningでしょう。pandasなどのライブラリを使っていると、FutureWarningはかなりの頻度で目にします。
自分で警告を出してみよう¶
警告は、見るだけでなく自分で出すこともできます。使うのはwarnings.warn()です。
第1引数にメッセージ、第2引数にカテゴリを渡します。カテゴリを省略すると、UserWarningになります。
どんなときに使うと便利なのでしょうか。たとえば、処理は続けられるけれど利用者に知らせておきたいことがある場面です。
先ほどのload関数のように、設定ファイルが無いので既定値で続ける、というケースが典型的です。止めるほどではないけれど、黙っているのも不親切だ、というときに警告がちょうどいい道具になります。
stacklevelで、呼び出した側の行を指す¶
先ほどのold_funcでは、stacklevel=2という引数を付けていました。これは、警告を出す場所として、どの行を表示するかを決める引数です。
既定の1のままだと、warnings.warn()を書いた行、つまり関数の中の行が表示されます。2にすると、その関数を呼び出した側の行が表示されます。
非推奨の警告を見た人が直すべきなのは、関数の中身ではなく、呼び出している自分のコードのほうです。だから、自作の関数で非推奨を伝えるならstacklevel=2を付けるのがおすすめです。
実際、先ほどの実行結果でも、DeprecationWarningのほうはold_func()を呼び出した15行目を指していました。stacklevelを付けていないUserWarningは、関数の中の12行目を指しています。
DeprecationWarningが出たり出なかったりする理由¶
ここで、少しふしぎな動きを紹介します。同じDeprecationWarningなのに、表示されるときとされないときがあるのです。
試しに、先ほどのold_funcを別のファイルmylib.pyに移し、そこにある別の関数から呼び出すようにしてみます。
# mylib.py
import warnings
def old_func():
warnings.warn("old_funcは非推奨です", DeprecationWarning, stacklevel=2)
def helper():
old_func()
そして、メインのスクリプトからmylib.helper()を呼びます。すると、警告は何も表示されず、最後のprintの結果だけが出ました。
これは故障ではなく、Pythonの初期設定によるものです。Python 3.7からは、DeprecationWarningは__main__、つまり直接実行したスクリプトのコードが原因のときだけ表示されるようになりました。
ライブラリの内部で起きたDeprecationWarningは、初期設定では表示されずに捨てられます。利用者が直せない警告で画面が埋まらないようにするための配慮です。
Python 3.13で初期設定の中身を確かめると、次のような順番でフィルターが並んでいました。
| 順番 | 動作 | 対象 |
|---|---|---|
| 1 | default(場所ごとに1回表示) | __main__で起きたDeprecationWarning |
| 2 | ignore(無視) | それ以外のDeprecationWarning |
| 3 | ignore(無視) | PendingDeprecationWarning |
| 4 | ignore(無視) | ImportWarning |
| 5 | ignore(無視) | ResourceWarning |
上から順に見ていき、最初に当てはまったものが使われます。UserWarningやFutureWarningはどれにも当てはまらないので、場所ごとに1回表示されます。
隠れた警告を見るには¶
では、ライブラリの中に隠れている警告を見たいときは、どうすればいいのでしょうか。いちばん手軽なのは、開発モードで実行することです。
python -X dev main.py
こうすると、先ほどは表示されなかったmylib.pyの警告が表示されました。python -W default::DeprecationWarning main.pyと書いても、同じように見られます。
私は10年ほどエンジニアとして開発に関わってきましたが、Pythonのバージョンを上げたとたんに動かなくなったコードを調べると、実はずっと前から非推奨の警告が隠れていた、ということが何度もありました。バージョンアップの前に一度この方法で警告を洗い出しておくと、当日の作業がぐっと楽になります。
バージョンアップの進め方については、こちらでも解説しています。【関連記事】Pythonのバージョンを変更する方法
警告を消す方法と、エラーに変える方法¶
警告の表示は、フィルターで変えられます。フィルターには、次のような動作を指定できます。
| 動作 | 意味 |
|---|---|
default |
場所ごとに最初の1回だけ表示する |
always |
毎回表示する |
once |
場所に関係なく、同じメッセージは1回だけ表示する |
module |
モジュールごとに1回だけ表示する |
ignore |
表示しない |
error |
例外に変えて、処理を止める |
設定する方法は大きく2つあります。コマンドラインで指定するか、コードの中で指定するかです。
コマンドラインで指定する¶
実行するときに-Wオプションを付けると、そのときだけフィルターを変えられます。たとえば-W errorを付けると、すべての警告が例外になります。
python -W error w.py
実際に試すと、最初のDeprecationWarningが例外として発生し、トレースバックを表示してそこで止まりました。doneは表示されません。
警告をエラーに変えると、どこから出たのかをトレースバックでたどれるようになります。トレースバックの読み方は、こちらで詳しく紹介しています。【関連記事】Pythonのトレースバックの読み方
同じ指定は、環境変数PYTHONWARNINGSでもできます。毎回オプションを書くのが面倒なときに便利です。
コードの中で指定する¶
コードの中では、warnings.filterwarnings()やwarnings.simplefilter()を使います。ただし、プログラム全体の警告を一律に消すのはおすすめしません。
本当に困るのは、消したことを忘れて、大事な警告まで見えなくなることです。そこで使いたいのが、warnings.catch_warnings()です。
with文の中だけフィルターを変え、抜けると元の設定に戻してくれます。
import warnings
with warnings.catch_warnings():
warnings.simplefilter("ignore", FutureWarning)
result = some_library_call() # この中のFutureWarningだけを消す
# withを抜けたら、元の設定に戻っている
消す範囲は、できるだけ狭くしておきましょう。どの警告をなぜ消したのか、コメントを1行残しておくと、あとで読む人が助かります。
以前、ログが警告で埋まって読みにくいという理由で、プログラムの先頭で全部の警告を無視する設定が入っていた現場がありました。その結果、ライブラリの更新で動きが変わることを知らせるFutureWarningも見逃し、集計結果がこっそり変わっていたのです。
警告をテストで確かめる¶
自分で警告を出すようにしたら、それが本当に出るかもテストで確かめておきたくなります。catch_warnings()にrecord=Trueを渡すと、出た警告をリストとして受け取れます。
import warnings
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
warnings.warn("a", UserWarning)
warnings.warn("b", DeprecationWarning)
print(len(caught), [w.category.__name__ for w in caught])
実行すると、2 ['UserWarning', 'DeprecationWarning']と表示されました。simplefilter("always")を入れているので、初期設定では捨てられるDeprecationWarningも記録されています。
pytestを使っているなら、pytest.warns()を使うともっと簡単に書けます。警告の種類とメッセージを指定して、出なければテストを失敗にできます。【関連記事】pytestの使い方
Python 3.13から使える@deprecated¶
最後に、比較的新しい機能を紹介します。Python 3.13から、warningsモジュールに@deprecatedというデコレーターが入りました。
関数やクラスの上に付けるだけで、呼び出されたときにDeprecationWarningを出してくれます。手元で確かめたところ、Python 3.12には無く、3.13には存在していました。
from warnings import deprecated
@deprecated("old_addは非推奨です。addを使ってください")
def old_add(a, b):
return a + b
print(old_add(1, 2))
Python 3.13で実行すると、old_add(1, 2)を呼んだ行を指して、指定したメッセージのDeprecationWarningが表示されました。そのあと、計算結果の3も表示されています。
このデコレーターの良いところは、実行時だけでなく、mypyなどの型チェッカーにも非推奨であることを伝えられる点です。エディターによっては、呼び出した場所に取り消し線を引いて知らせてくれます。
3.12以前でも同じことをしたい場合は、外部パッケージのtyping_extensionsに同じ名前のデコレーターがあります。プロジェクトで使っているPythonのバージョンに合わせて選びましょう。
まとめ¶
警告は、処理を止めずに注意だけを伝える仕組みです。エラーと違って見逃しやすいぶん、出たときに読めるようになっておくと、トラブルを早めに防げます。
大事なポイントは、次の3つです。自作の非推奨はstacklevel=2で呼び出し側を指すこと、ライブラリのDeprecationWarningは初期設定では隠れていること、そして警告を消すならcatch_warningsで範囲を狭くすること。
画面に警告が出ても、もう慌てる必要はありません。まずはファイル名と行番号、そしてカテゴリを読んでみてください。
ここまでお読みいただきありがとうございました。