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.
| Level | Integer | When to use it |
|---|---|---|
| DEBUG | 10 | Detailed internal state — function arguments, loop counters, cache hits. Emit freely in development; always off in production. |
| INFO | 20 | Normal operational events — service started, request completed, user logged in. Safe to enable in production when you need a narrative of what the system did. |
| WARNING | 30 | Something 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. |
| ERROR | 40 | An 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. |
| CRITICAL | 50 | A 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
WARNINGand above for anything an operator needs to see; reserveDEBUGfor local tracing and turn it off in production. exc_info=Trueonerror/exceptioncalls attaches the active traceback.- Setting a logger to
NOTSETmakes 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
DEBUGlogger with anINFOhandler still dropsDEBUGoutput 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:
- Logger gate — the record’s
levelnomust be ≥ the logger’s effective level (its own level, or the nearest ancestor’s if it isNOTSET). - 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
- Python documentation —
logging— Logging facility for Python — level constants andsetLevelsemantics. - Python documentation — Logging HOWTO: Logging Levels — the numeric values and the two-gate flow.