閉じる

Pythonでprint()デバッグは卒業!loggingの基本設定と使い方

開発中にとりあえずprint()で値を出すというやり方は手軽ですが、規模が大きくなると追跡や管理が難しくなります。

本記事では、Python標準のloggingモジュールを使って、読みやすく制御しやすいログを書くための基本を丁寧に解説します。

print()からの置き換え方、設定のコツ、よくあるハマりどころまで実例で紹介します。

Pythonでprintデバッグを卒業する理由

print()は動作確認に役立ちますが、ログとして使うには限界があります。

出力の重要度に差をつけられず、オンオフの切り替えや保存先の切り替えが難しいためです。

また、日時やモジュール名が自動で付かないので、後から読み返すと何の出力か分かりにくくなります。

ログは「記録」と「解析」が目的であり、出力の粒度や書式を統一することが重要です。

printデバッグの主な課題

  • 出力の重要度を区別できないため、ノイズが増えやすいです。
  • 一括で出力を抑制したり、必要なときだけ詳細化する切り替えが難しいです。
  • ファイル保存、ローテーション、エンコーディングなどの運用要件に対応しづらいです。
  • スレッドやプロセスが増えると、出力が混ざって読みづらくなります。

loggingのメリット(レベル管理・保存・切り替え)

loggingモジュールは、レベル管理出力先の切り替え書式の統一が標準機能でそろっています。

設定は1カ所に集約でき、アプリ全体で一貫したログが得られます。

本番ではINFO以上、開発時はDEBUGまでなど、状況に応じた切り替えが容易です。

さらに、例外情報(スタックトレース)の自動出力や、ファイル保存のエンコーディング指定などの実運用の要件も満たせます。

loggingの基本設定(basicConfig)

1行で始めるlogging.basicConfigの書き方

最小構成はbasicConfigの1行です。

レベルをINFO以上にして、試しに出力してみましょう。

Python
# logging_basic_minimal.py
import logging

# 最小構成: INFO以上を表示
logging.basicConfig(level=logging.INFO)

logging.debug("これは表示されません(DEBUGは非表示)")  # レベルがINFOなので出ない
logging.info("アプリ起動")  # これは出る
logging.warning("注意: 設定値がデフォルトです")
実行結果
INFO:root:アプリ起動
WARNING:root:注意: 設定値がデフォルトです

デフォルトの出力先はコンソール(stderr)で、書式はLEVEL:name:messageです。

ログレベル(level)の使い分け(debug/info/warning/error/critical)

loggingには目的別のレベルがあります。

数値が小さいほど詳細で、設定したレベル以上だけが出力されます。

レベル一覧(用途の目安):

レベル名数値用途の目安
DEBUG10開発時の詳細情報。変数値や分岐の追跡に使います。
INFO20通常の動作記録。起動、終了、主要イベント。
WARNING30想定外だが処理継続可能な状況。非推奨の使用など。
ERROR40処理を続けにくいエラー。例外の発生など。
CRITICAL50即時対応が必要な重大障害。

例を動かすと、設定レベルによる見え方が分かります。

Python
# logging_levels_demo.py
import logging

logging.basicConfig(level=logging.WARNING)  # WARNING以上だけ表示

logging.debug("DEBUG: 詳細")
logging.info("INFO: 情報")
logging.warning("WARNING: 警告")
logging.error("ERROR: エラー")
logging.critical("CRITICAL: 致命的")
実行結果
WARNING:root:WARNING: 警告
ERROR:root:ERROR: エラー
CRITICAL:root:CRITICAL: 致命的

スクリプトの先頭に書く位置と注意点

ログ設定はアプリの「入口」で1度だけ行うのが鉄則です。

モジュール側では設定せず、入口スクリプト(例: main.py)でbasicConfigします。

Python
# mylib.py (ライブラリ側: 設定はしない)
import logging
logger = logging.getLogger(__name__)  # __name__でモジュール名付きロガーを取得

def run():
    logger.info("mylib.run() を開始")
    logger.debug("内部状態 x=42")
Python
# main.py (エントリポイント)
import logging
from mylib import run

def main():
    logging.basicConfig(
        level=logging.INFO,  # 入口で1回だけ設定
        format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        datefmt="%H:%M:%S",
    )
    run()

if __name__ == "__main__":
    main()
実行結果
12:00:00 [INFO] mylib: mylib.run() を開始
注意:

Jupyterや再実行環境では、既に設定済みだとbasicConfigが効かないことがあります。

Python 3.8+ならbasicConfig(..., force=True)で再設定できます。

loggerの取得(getLogger)の基本

getLogger(name)でロガーを取得します。

慣例的に__name__を使い、モジュール単位の名前空間を持たせます。

ロガーは階層構造で、親の設定を継承します。

Python
# logger_getting.py
import logging

# 入口で一度だけ設定
logging.basicConfig(level=logging.INFO)

# 名前付きロガーを取得
app_logger = logging.getLogger("myapp")
db_logger = logging.getLogger("myapp.db")

app_logger.info("アプリの情報ログ")
db_logger.warning("DB接続が遅延しています")
実行結果
INFO:myapp:アプリの情報ログ
WARNING:myapp.db:DB接続が遅延しています

ログの出力先とフォーマット

コンソールに出力する(デフォルト)

何も指定しない場合、stderrへのコンソール出力が使われます。

学習や開発ではまずこれで十分です。

Python
# console_default.py
import logging

logging.basicConfig(level=logging.INFO)  # デフォルトはStreamHandler(stderr)
logging.info("コンソールに出力されます")
実行結果
INFO:root:コンソールに出力されます

ファイルに保存する(filename/encoding)

ファイル保存はfilenameencodingを指定します。

日本語ログはUTF-8で保存するのが安全です。

Python
# file_logging.py
import logging

logging.basicConfig(
    level=logging.INFO,
    filename="app.log",          # ログファイル名
    filemode="w",                # "a"なら追記、"w"なら毎回上書き
    encoding="utf-8",            # 日本語を安全に保存
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logging.info("ファイルに保存されました")
logging.warning("警告もファイルに残ります")
実行結果app.log
2025-01-01 12:00:00 [INFO] root: ファイルに保存されました
2025-01-01 12:00:00 [WARNING] root: 警告もファイルに残ります

ログ書式(format/datefmt)の基本

書式はformatで、日時はdatefmtで指定します。

よく使う書式は次の通りです。

Python
# format_basic.py
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s - %(message)s",
    datefmt="%H:%M:%S",
)

logging.info("書式付きのログ")
実行結果
12:00:00 [INFO] root - 書式付きのログ

タイムスタンプやモジュール名を含める

formatで使える代表的なプレースホルダです。

必要な情報を選んで組み合わせます。

プレースホルダ意味
%(asctime)s日時(datefmtで整形)
%(levelname)sログレベル名
%(name)sロガー名(getLoggerで付けた名前)
%(module)sモジュール名(ファイル名から拡張子抜き)
%(filename)sファイル名
%(funcName)s関数名
%(lineno)d行番号
%(message)sログ本文
%(process)dプロセスID
%(threadName)sスレッド名

例えば、トラブル調査では%(asctime)s %(levelname)s %(name)s:%(lineno)dのように、日時・レベル・モジュール・行番号を含めると有益です。

コンソールとファイルに同時出力する例

複数出力はhandlers引数を使います。

Python
# multi_output.py
import logging

console = logging.StreamHandler()  # コンソール
file = logging.FileHandler("app.log", encoding="utf-8")  # ファイル

logging.basicConfig(
    level=logging.INFO,
    handlers=[console, file],  # 複数ハンドラ
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)

logging.info("コンソールとファイルに同時出力")
実行結果
12:00:00 [INFO] root: コンソールとファイルに同時出力
textapp.log
12:00:00 [INFO] root: コンソールとファイルに同時出力

printからloggingへの置き換え実例

printをlogging.infoに置換するコツ

まずはprint()を同じ意味のレベルに置き換えることから始めます。

通常の進捗や状態はinfo、詳細な変数確認はdebugにします。

置き換え前

Python
# before_print.py
def process(items):
    print("処理を開始します")  # 情報
    for i, x in enumerate(items):
        print(f"{i}番目: x={x}")  # 詳細
    print("処理が完了しました")  # 情報

置き換え後(基本形)

Python
# after_logging_basic.py
import logging
logger = logging.getLogger(__name__)

def process(items):
    logger.info("処理を開始します")  # 情報レベル
    for i, x in enumerate(items):
        logger.debug("index=%d, x=%s", i, x)  # 文字列整形は引数で渡すのが基本
    logger.info("処理が完了しました")

ここで文字列整形はf文字列ではなく引数渡し(例: logger.debug("x=%s", x))を推奨します。

DEBUGが抑制される設定では文字列整形自体が実行されず、高速かつ無駄がありません。

もちろん学習初期はf文字列でも動きますが、運用では引数渡しを習慣化すると良いです。

動作例:

Python
# run_process.py
import logging
from after_logging_basic import process

logging.basicConfig(level=logging.INFO)
process([10, 20])

# DEBUGは見えないが、INFOは出る
実行結果
INFO:after_logging_basic:処理を開始します
INFO:after_logging_basic:処理が完了しました

エラーはlogging.error/exceptionで記録する(exc_info)

例外を伴うエラーはlogger.exception()が簡単です。

except節内で呼ぶと自動でスタックトレースが付きます。

Python
# log_exception.py
import logging
logger = logging.getLogger(__name__)

def divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        logger.exception("0除算が発生しました")  # 例外情報付きでERRORレベル出力
        return None

if __name__ == "__main__":
    logging.basicConfig(level=logging.INFO)
    divide(1, 0)
実行結果
ERROR:__main__:0除算が発生しました
Traceback (most recent call last):
  File "log_exception.py", line 8, in divide
    return a / b
ZeroDivisionError: division by zero
補足

logger.error("失敗", exc_info=True)でもトレースを付けられます。

通常はexception()が最短です。

一時的に出力を増やす(levelを変更)

開発中だけ詳細ログを見たいときは、入口でレベルを切り替えられるようにします。

Python
# toggle_level.py
import argparse
import logging

logger = logging.getLogger(__name__)

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--debug", action="store_true", help="DEBUGログを有効化")
    args = parser.parse_args()

    level = logging.DEBUG if args.debug else logging.INFO
    logging.basicConfig(level=level, format="%(levelname)s:%(message)s")

    logger.debug("詳細: 初期化パラメータ x=42")
    logger.info("アプリ開始")

if __name__ == "__main__":
    main()
実行結果
# 通常起動
INFO:アプリ開始

# --debug 付き
DEBUG:詳細: 初期化パラメータ x=42
INFO:アプリ開始

既に設定済みで動的に増やしたい場合はlogger.setLevel(logging.DEBUG)も有効です。

特定の名前空間だけ詳細化したいときに便利です。

basicConfigが効かない時のチェック(重複設定)

「basicConfigを変えても出力が変わらない」ときは、すでにロガーにハンドラが付いている可能性があります。

原因と対処を確認しましょう。

よくある原因

  • 以前の実行や他ライブラリが、すでにルートロガーにハンドラを追加している。
  • JupyterやREPLで同じセルを何度も実行している。
  • 複数箇所でbasicConfigを呼んでいる。

状態の確認

Python
# check_handlers.py
import logging
root = logging.getLogger()  # ルートロガー
print("hasHandlers:", root.hasHandlers())
print("handlers:", root.handlers)
実行結果
hasHandlers: True
handlers: [<StreamHandler (NOTSET)>]

代表的な対処

  • Python 3.8+ : logging.basicConfig(..., force=True)で既存ハンドラをクリアして再設定。
  • それ以前 : 既存ハンドラを手動で外す、またはhandlers=[...]で明示設定。
Python
# reconfigure.py
import logging

# 既存ハンドラを強制的に置き換え(Python 3.8+)
logging.basicConfig(
    level=logging.DEBUG,
    format="%(levelname)s:%(message)s",
    force=True,  # これがポイント
)

logging.debug("再設定後はDEBUGも表示されます")
実行結果
DEBUG:再設定後はDEBUGも表示されます
補足

ライブラリ側ではbasicConfigを呼ばず、logger = logging.getLogger(__name__)でロガーを作るだけにし、アプリの入口で一元設定するのがベストプラクティスです。

まとめ

print()は手軽ですが、運用や解析まで見据えるとloggingでの一元的な記録と制御が不可欠です。

この記事ではbasicConfigでの初期設定、レベルの使い分け、出力先や書式の指定、getLogger(__name__)によるモジュール別ロガーの利用、例外の記録(exception)、そして設定が効かないときの原因と対処(force=Trueなど)までを実例で紹介しました。

まずは入口スクリプトでINFOレベルの設定から始め、必要に応じてDEBUGやファイル保存、書式の拡張へと進めてください。

print()から1歩進んだ、読みやすく保守しやすいログで、デバッグと運用を快適にしていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!