Python Logging Levels Reference

Python logging level names, numeric values and when to use each level.

Reference for the Python logging module severity levels — DEBUG, INFO, WARNING, ERROR, CRITICAL — with their numeric values, NOTSET semantics and a live threshold filter showing which records each setLevel emits. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

What are the numeric values of Python logging levels?

NOTSET is 0, DEBUG is 10, INFO is 20, WARNING is 30, ERROR is 40 and CRITICAL is 50. They are spaced by 10 so you can insert custom levels in between, such as 15 or 25.

Python’s five standard logging severities

The Python standard-library logging module defines five named severity levels plus the NOTSET sentinel. Each level maps to an integer, and a record is only emitted when its value clears the threshold configured with setLevel. This reference lists every level, its numeric value and when to reach for it, and a live filter shows exactly which records a chosen threshold will let through.

How the threshold gate works

Every log record carries a numeric levelno. When you call log.info(...), the record is created with value 20. The record is emitted only if its value is greater than or equal to the logger’s effective level — and then again only if it clears each attached handler’s own level. Because the named levels are spaced by 10, you can register intermediate custom levels:

import logging
TRACE = 15
logging.addLevelName(TRACE, "TRACE")
logging.getLogger().setLevel(TRACE)  # now DEBUG is hidden but TRACE shows

The root logger defaults to WARNING, so DEBUG and INFO are dropped until you lower the threshold via logging.basicConfig(level=logging.DEBUG).

When to use each level

Choosing the right level matters because it determines what appears in production logs — and noisy logs are as dangerous as silent ones.

LevelIntegerWhen to use it
DEBUG10Detailed internal state — function arguments, loop counters, cache hits. Emit freely in development; always off in production.
INFO20Normal operational events — service started, request completed, user logged in. Safe to enable in production when you need a narrative of what the system did.
WARNING30Something unexpected happened but the system recovered — a config value is missing so a default was used; a deprecated API path was called. This is the default threshold.
ERROR40An operation failed and the system could not recover from it automatically — a database write failed, a third-party API returned a 500. Attach exc_info=True to capture the traceback.
CRITICAL50A failure that may prevent the application from continuing — data corruption, inability to bind to a port, loss of a critical service dependency. Often triggers an alert.

Setting up logging correctly

A minimal but effective configuration for an application:

import logging

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

logger = logging.getLogger(__name__)

Use getLogger(__name__) in every module so log records carry the module name. This makes it easy to silence a noisy library without affecting your own code:

# Suppress debug output from a verbose library
logging.getLogger("urllib3").setLevel(logging.WARNING)

Exception logging

The cleanest way to log an exception with its traceback is logger.exception(msg) inside an except block — it logs at ERROR level and automatically attaches the active traceback. Equivalent to logger.error(msg, exc_info=True):

try:
    result = compute()
except ValueError as err:
    logger.exception("Computation failed for input %s", input_val)
    # traceback is attached automatically

Tips and notes

  • Use WARNING and above for anything an operator needs to see; reserve DEBUG for local tracing and turn it off in production.
  • exc_info=True on error/exception calls attaches the active traceback.
  • Setting a logger to NOTSET makes it inherit its parent — handy for opting a noisy library logger back into the application’s global level.
  • Handler and logger levels are independent gates; a DEBUG logger with an INFO handler still drops DEBUG output at the handler.
  • Never use logging.disable() in production code — it makes the threshold invisible and is easy to forget to remove.

The two-gate model, step by step

Output only appears when a record clears both independent gates:

  1. Logger gate — the record’s levelno must be ≥ the logger’s effective level (its own level, or the nearest ancestor’s if it is NOTSET).
  2. Handler gate — the record must then clear each attached handler’s own level.

A frequent surprise: setting the logger to DEBUG but leaving the handler at its default WARNING means DEBUG records are created, pass the logger gate, and are then dropped by the handler. Both must be lowered to see debug output:

logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)          # gate 1
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)         # gate 2 — easy to forget
logger.addHandler(handler)

Propagation and effective level

Loggers form a dotted-name hierarchy (myapp.db.pool is a child of myapp.db, which is a child of myapp). A child left at NOTSET walks up the tree until it finds an ancestor with an explicit level — that is its effective level. Records also propagate up to ancestor handlers by default, which is why a single handler on the root logger can capture output from every module. Set logger.propagate = False on a child to stop it duplicating records into ancestor handlers.

Sources