Why does a logger without handlers emit logging.WARNING and above?

Consider:

import logging
logger = logging.getLogger(__name__)
print(logger.handlers)
print(logger.parent.handlers)
print(logger.parent.parent)
logger.warning("Hello from warning")

Output:

[]
[]
None
Hello from warning

This confuses me. Neither the logger nor its parent have any handlers, yet the error message is still printed to stderr. By what?

Good question. There’s no default handler visible:

>>> import logging
>>> logging.getHandlerNames()
frozenset()
>>> logging.warning('Oh no!')
WARNING:root:Oh no!
>>> logging.getHandlerNames()
frozenset()

The answer is in the logging tutorial in the docs:

By default, no destination is set for any logging messages. You can specify a destination (such as console or file) by using basicConfig() as in the tutorial examples. If you call the functions debug(), info(), warning(), error() and critical(), they will check to see if no destination is set; and if one is not set, they will set a destination of the console (sys.stderr ) and a default format for the displayed message before delegating to the root logger to do the actual message output.

The RootLogger The Logger.callHandlers method uses a “last resort handler” _StderrHandler(WARNING) that emits it.

After going through this diagram propagating the message, it gets to the root logger, finds no handlers there either, but then there is the following

if lastResort:
    if record.levelno >= lastResort.level:
        lastResort.handle(record)
    elif raiseExceptions and not self.manager.emittedNoHandlerWarning:
        sys.stderr.write("No handlers could be found for logger"
            " \"%s\"\n" % self.name)

where

_defaultLastResort = _StderrHandler(WARNING)
lastResort = _defaultLastResort

The documentation mentions it here.

2 Likes

That explains it! Except, if I understand correctly, it is not RootLogger that has lastResort, but the logging module itself. So, any logger with no handlers will use lastResort, without needing to go via RootLogger:

import logging
logger = logging.getLogger(__name__)
logger.propagate = False  # Don't send LogRecords to RootLogger
logger.warning("Hello from warning")

This still prints to stderr.

Ah, yes the use of the lastResort is in the Logger.callHandlers.