Python 6 min read

Python logging levels

What each level means, which one to use, and how to set it from code, a flag or an environment variable.

By JeremyFunk

Python's built-in logging module tags every message with a log level: DEBUG, INFO, WARNING, ERROR or CRITICAL. The level you set decides which messages are written, so you can keep detailed output while developing and only the important messages in production.

This guide explains what each level is for, how to set the level in code, from a command-line flag or from an environment variable, how logger and handler levels work together, and how levels carry over when you send Python logs to OpenTelemetry.

Key takeaways

  • Python's default level is WARNING, so info() and debug() print nothing until you set one.
  • Run production at INFO and switch to DEBUG only while you investigate.
  • Read the level from a --log-level flag or a LOG_LEVEL variable, so changing it needs no code edit.
  • basicConfig does nothing if logging is already set up: pass force=True or set the root logger's level.
  • Set library loggers such as urllib3 to WARNING to hide their DEBUG and INFO lines.

01 · Basics

The Python logging levels

The method you call sets a message's level: log.debug(), log.info(), log.warning(), log.error() or log.critical(). These are the five levels, from least to most serious:

LevelUse it when
DEBUG Details you only need while tracking down a bug.log.debug("cache miss for user 42")
INFO Normal events that show the program is doing its job.log.info("order 1042 placed")
WARNING Something unexpected happened, and the program handled it.log.warning("payment API slow, retrying")
ERROR An operation failed.log.error("charge failed for order 1042")
CRITICAL The program can't keep running.log.critical("database unreachable, exiting")

Set the level

Set the level once, when the program starts, with logging.basicConfig. Messages at that level or above are printed, and messages below it are dropped:

app.py

import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger("app")

log.debug("cache miss for user 42")
log.info("order 1042 placed")
log.warning("payment API slow, retrying")
$ python app.py
INFO:app:order 1042 placed
WARNING:app:payment API slow, retrying

logging.getLogger("app") returns a logger named app, the object you log through. The DEBUG message is dropped because DEBUG is below INFO.

Without the basicConfig line, the script prints only payment API slow, retrying. Python's default level is WARNING, which is why log.info() and log.debug() print nothing until you set a level.

To change the level for one part of your program, call setLevel on its logger. This line turns on DEBUG messages for the app.db logger while everything else stays at INFO:

logging.getLogger("app.db").setLevel(logging.DEBUG)

Choosing a level

Ask these questions in order and use the first level that fits:

  1. Can the program no longer keep running?CRITICAL
  2. Did an operation fail?ERROR
  3. Did something go wrong that the program recovered from?WARNING
  4. Would you want this line in production logs every day?INFO
  5. Is it only useful while debugging?DEBUG

Run production at INFO and switch to DEBUG while you investigate a problem.

02 · Configure

Set the level from a flag or environment variable

With logging.INFO in the code, seeing DEBUG messages means editing the code. Read the level from outside instead: a command-line tool takes a flag, and a service in a container reads an environment variable. Python reads neither on its own, so the program passes the value to basicConfig, which accepts a level name such as "DEBUG" as well as the constant:

app.py

import argparse
import logging

parser = argparse.ArgumentParser()
parser.add_argument(
    "--log-level",
    default="WARNING",
    type=str.upper,
    choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
)
args = parser.parse_args()

logging.basicConfig(level=args.log_level)
log = logging.getLogger("app")
log.debug("parsed %s", args)
log.info("starting")
$ python app.py --log-level debug
DEBUG:app:parsed Namespace(log_level='DEBUG')
INFO:app:starting
$ python app.py --log-level loud
usage: app.py [-h] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}]
app.py: error: argument --log-level: invalid choice: 'LOUD' (choose from 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')

type=str.upper makes --log-level debug work, and choices rejects a typo before the program starts.

worker.py

import logging
import os

logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper())
log = logging.getLogger("worker")
log.debug("polling queue")
log.info("job 7 done")
$ LOG_LEVEL=debug python worker.py
DEBUG:worker:polling queue
INFO:worker:job 7 done
$ python worker.py
INFO:worker:job 7 done

LOG_LEVEL is a common name, not a variable Python knows about. Keep the .upper(): basicConfig accepts "DEBUG" but raises ValueError for "debug", and for an unknown name such as LOG_LEVEL=loud.

basicConfig runs once

If a framework or library already set up logging, basicConfig silently does nothing. Pass force=True to replace the existing setup, or set the level directly instead:

logging.getLogger().setLevel(logging.INFO)

Quiet noisy libraries

Libraries log through their own loggers, named after the package. Raise the level on those loggers to hide their DEBUG and INFO messages when you run your own code at DEBUG:

for name in ("urllib3", "botocore", "httpx", "httpcore"):
    logging.getLogger(name).setLevel(logging.WARNING)

A level on urllib3 also covers urllib3.connectionpool and every other logger whose name starts with urllib3.. The default basicConfig output shows each logger's name between the colons, as in DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): example.com:443.

03 · Going further

Logger and handler levels

A handler is the object that writes messages somewhere: StreamHandler to the terminal, FileHandler to a file. basicConfig adds a terminal handler for you. Each handler can have its own level, so this script prints warnings to the terminal and keeps every message in a file:

app.py

import logging

root = logging.getLogger()
root.setLevel(logging.DEBUG)

console = logging.StreamHandler()
console.setLevel(logging.WARNING)
root.addHandler(console)

logfile = logging.FileHandler("app.log")
logfile.setLevel(logging.DEBUG)
root.addHandler(logfile)

log = logging.getLogger("app")
log.debug("cache miss for user 42")
log.warning("payment API slow, retrying")
$ python app.py
payment API slow, retrying
$ cat app.log
cache miss for user 42
payment API slow, retrying

A message passes the logger's level first, then each handler's level. A message the logger drops never reaches a handler, so set the logger to the lowest level any of its handlers needs.

04 · OpenTelemetry

Python log levels in OpenTelemetry

OpenTelemetry can send your logging messages, each with its level, to any backend that accepts OTLP, the OpenTelemetry protocol. The setup is one more handler on the root logger, so the levels on this page decide what gets sent. The example sends to Maple.

Install and configure

pip install opentelemetry-sdk \
    opentelemetry-exporter-otlp-proto-http \
    opentelemetry-instrumentation-logging

Create telemetry.py. It names your service, points the exporter at your backend's OTLP logs endpoint, and adds two handlers to the root logger: the terminal gets every level, the backend gets INFO and above.

telemetry.py

import logging

from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.instrumentation.logging.handler import LoggingHandler
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.resources import Resource

ENDPOINT = "https://ingest.maple.dev/v1/logs"
HEADERS = {"authorization": "Bearer YOUR_INGEST_KEY"}

logger_provider = LoggerProvider(resource=Resource.create({"service.name": "my-python-app"}))
logger_provider.add_log_record_processor(
    BatchLogRecordProcessor(OTLPLogExporter(endpoint=ENDPOINT, headers=HEADERS))
)
set_logger_provider(logger_provider)

root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.addHandler(logging.StreamHandler())  # terminal: every level

otel = LoggingHandler(logger_provider=logger_provider)
otel.setLevel(logging.INFO)  # backend: INFO and above
root.addHandler(otel)

# The exporter's HTTP client logs each request at DEBUG.
logging.getLogger("urllib3").setLevel(logging.WARNING)

For Maple, replace YOUR_INGEST_KEY with an ingest key from the Ingestion page in Maple's settings; organizations in the EU region send to https://ingest.eu.maple.dev/v1/logs. For another backend, change ENDPOINT and HEADERS: a local OpenTelemetry Collector, for example, listens on http://localhost:4318/v1/logs and needs no header.

Run it

Import telemetry once, before the first log call:

app.py

import logging

import telemetry  # noqa: F401

log = logging.getLogger("app")
log.debug("cart has 3 items")
log.info("order 1042 placed")
log.warning("payment API slow, retrying")
$ python app.py
cart has 3 items
order 1042 placed
payment API slow, retrying

All three lines print in the terminal. The backend receives order 1042 placed and payment API slow, retrying, and the DEBUG line stays local. The SDK sends records in batches about once a second and sends the rest when the program exits.

See the levels in Maple

  1. 01

    Open Logs and pick a level

    Pick your app under Service in the sidebar, then include or exclude levels under Severity. The volume chart stacks log counts by level, in the colors this page uses.

    The Logs page for the my-python-app service with the WARN and ERROR severities selected: the volume chart stacked by severity and the matching rows, such as slow query warnings and charge failed errors.
  2. 02

    Open a record

    Click a row, then Open detail to see the message and its attributes. A record logged with log.exception() carries the exception type, message and stack trace.

    The detail sheet of an ERROR record from a Python service: the Error banner with the TimeoutError, the meta strip with the trace link and span ID, and the exception message, stack trace and type under Log Attributes.

Python logs in Maple

Send your logging records to Maple

For traces and metrics from the same app, see the Python guide. Using Loguru instead of logging? See the Loguru guide.

05 · Mistakes

Common Python logging level mistakes

01

Lower-case level names

setLevel("debug") and basicConfig(level="debug") raise ValueError: Unknown level: 'debug'. Call .upper() on any name that comes from a user or a variable.

02

Losing the traceback

log.error(e) in an except block logs the message without the traceback. log.exception("charge failed") logs at ERROR and adds the traceback.

03

Configuring logging in a library

A library that calls basicConfig or sets the root level overrides the application's choice. A library only creates its logger with logging.getLogger(__name__), and the application sets the levels.

04

Formatting the message yourself

log.debug(f"payload {data}") builds the string even when DEBUG is off. log.debug("payload %s", data) only builds it for messages that pass the level.

Sources and further reading

FAQ

よくある質問

What are the Python logging levels?
Python's logging module has five levels, from least to most serious: DEBUG, INFO, WARNING, ERROR and CRITICAL. Each has a constant (logging.DEBUG, logging.INFO, ...) and a method (log.debug(), log.info(), ...). Python drops every message below the level you set.
What is the default logging level in Python?
WARNING. Until you set a level, Python drops DEBUG and INFO messages, which is why log.info() prints nothing in a new script. Call logging.basicConfig(level=logging.INFO) at startup to change it.
How do I set the logging level in Python?
Call logging.basicConfig(level=logging.INFO) once at startup. To change the level of one logger, call setLevel on it: logging.getLogger("app.db").setLevel(logging.DEBUG). Both accept the constant or its upper-case name, such as "DEBUG".
How do I set the Python log level from an environment variable?
Read the variable yourself and pass it on: logging.basicConfig(level=os.environ.get("LOG_LEVEL", "INFO").upper()). Python does not read any log level variable on its own; LOG_LEVEL is a naming convention. An unknown name raises ValueError at startup.
How do I set the Python logging level from the command line?
Add a --log-level option with argparse, using type=str.upper so lower-case input works and choices to reject typos, then pass args.log_level to logging.basicConfig(level=...).
Why doesn't my Python debug log print?
The level is still above DEBUG, usually at the default WARNING. Call logging.basicConfig(level=logging.DEBUG) at startup. If that has no effect, a framework or library already set up logging, and basicConfig does nothing in that case; call logging.getLogger().setLevel(logging.DEBUG) instead.
What is the difference between logger level and handler level?
The logger level decides whether a message is logged at all. Each handler's level then decides whether that handler writes it, so one logger can send DEBUG to a file and only WARNING to the terminal.

今日、最初のトレースを。

SDKを追加し、OTLPエクスポーターをMapleに向ければトレースが届きます。

maple.dev:OpenTelemetryの上のオブザーバビリティ