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)
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")