開発中にとりあえずprint()で値を出すというやり方は手軽ですが、規模が大きくなると追跡や管理が難しくなります。
本記事では、Python標準のloggingモジュールを使って、読みやすく制御しやすいログを書くための基本を丁寧に解説します。
print()からの置き換え方、設定のコツ、よくあるハマりどころまで実例で紹介します。
print()デバッグからloggingへ
Pythonでprintデバッグを卒業する理由
print()は動作確認に役立ちますが、ログとして使うには限界があります。
出力の重要度に差をつけられず、オンオフの切り替えや保存先の切り替えが難しいためです。
また、日時やモジュール名が自動で付かないので、後から読み返すと何の出力か分かりにくくなります。
ログは「記録」と「解析」が目的であり、出力の粒度や書式を統一することが重要です。
printデバッグの主な課題
- 出力の重要度を区別できないため、ノイズが増えやすいです。
- 一括で出力を抑制したり、必要なときだけ詳細化する切り替えが難しいです。
- ファイル保存、ローテーション、エンコーディングなどの運用要件に対応しづらいです。
- スレッドやプロセスが増えると、出力が混ざって読みづらくなります。
loggingのメリット(レベル管理・保存・切り替え)
loggingモジュールは、レベル管理、出力先の切り替え、書式の統一が標準機能でそろっています。
設定は1カ所に集約でき、アプリ全体で一貫したログが得られます。
本番ではINFO以上、開発時はDEBUGまでなど、状況に応じた切り替えが容易です。
さらに、例外情報(スタックトレース)の自動出力や、ファイル保存のエンコーディング指定などの実運用の要件も満たせます。
loggingの基本設定(basicConfig)
1行で始めるlogging.basicConfigの書き方
最小構成はbasicConfig
の1行です。
レベルをINFO以上にして、試しに出力してみましょう。
# 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には目的別のレベルがあります。
数値が小さいほど詳細で、設定したレベル以上だけが出力されます。
レベル一覧(用途の目安):
レベル名 | 数値 | 用途の目安 |
---|---|---|
DEBUG | 10 | 開発時の詳細情報。変数値や分岐の追跡に使います。 |
INFO | 20 | 通常の動作記録。起動、終了、主要イベント。 |
WARNING | 30 | 想定外だが処理継続可能な状況。非推奨の使用など。 |
ERROR | 40 | 処理を続けにくいエラー。例外の発生など。 |
CRITICAL | 50 | 即時対応が必要な重大障害。 |
例を動かすと、設定レベルによる見え方が分かります。
# 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
します。
# mylib.py (ライブラリ側: 設定はしない)
import logging
logger = logging.getLogger(__name__) # __name__でモジュール名付きロガーを取得
def run():
logger.info("mylib.run() を開始")
logger.debug("内部状態 x=42")
# 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__
を使い、モジュール単位の名前空間を持たせます。
ロガーは階層構造で、親の設定を継承します。
# 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へのコンソール出力が使われます。
学習や開発ではまずこれで十分です。
# console_default.py
import logging
logging.basicConfig(level=logging.INFO) # デフォルトはStreamHandler(stderr)
logging.info("コンソールに出力されます")
INFO:root:コンソールに出力されます
ファイルに保存する(filename/encoding)
ファイル保存はfilename
とencoding
を指定します。
日本語ログはUTF-8で保存するのが安全です。
# 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("警告もファイルに残ります")
2025-01-01 12:00:00 [INFO] root: ファイルに保存されました
2025-01-01 12:00:00 [WARNING] root: 警告もファイルに残ります
ログ書式(format/datefmt)の基本
書式はformat
で、日時はdatefmt
で指定します。
よく使う書式は次の通りです。
# 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
引数を使います。
# 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: コンソールとファイルに同時出力
12:00:00 [INFO] root: コンソールとファイルに同時出力
printからloggingへの置き換え実例
printをlogging.infoに置換するコツ
まずはprint()を同じ意味のレベルに置き換えることから始めます。
通常の進捗や状態はinfo
、詳細な変数確認はdebug
にします。
置き換え前
# before_print.py
def process(items):
print("処理を開始します") # 情報
for i, x in enumerate(items):
print(f"{i}番目: x={x}") # 詳細
print("処理が完了しました") # 情報
置き換え後(基本形)
# 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文字列でも動きますが、運用では引数渡しを習慣化すると良いです。
動作例:
# 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節内で呼ぶと自動でスタックトレースが付きます。
# 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()
が最短です。
- Logger.exception — Python 3.13.7 ドキュメント
- 関連記事:例外(Exception)入門: 実行時エラーの正しい理解
- 関連記事:PythonのTracebackの読み方と直し方: エラーメッセージについて解説
一時的に出力を増やす(levelを変更)
開発中だけ詳細ログを見たいときは、入口でレベルを切り替えられるようにします。
# 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
を呼んでいる。
状態の確認
# 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=[...]
で明示設定。
# 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歩進んだ、読みやすく保守しやすいログで、デバッグと運用を快適にしていきましょう。