Python Logging Hierarchy and Log Propagation

Python's logging module uses a hierarchical namespace to manage loggers and route messages through an application. By using dot-separated naming conventions, loggers form parent-child relationships where records generated by child loggers are automatically forwarded up the tree to their ancestors through a process called propagation. This architecture centralizes handler configuration, eliminates redundant setup, and gives developers granular control over log levels across different modules.

The Hierarchical Logger Tree

The logger hierarchy is structured like a filesystem or Python module path, using periods (.) as delimiters. At the base of every hierarchy sits the Root Logger, which has no name (or is referenced as an empty string).

When you instantiate a logger:

import logging

root = logging.getLogger()           # Root logger
parent = logging.getLogger("app")    # Child of root
child = logging.getLogger("app.db")  # Child of "app", grandchild of root

By convention, Python developers use logging.getLogger(__name__). This automatically maps the logging hierarchy to the application's package structure (e.g., mypackage.services.auth), ensuring that module-level loggers inherit behavior based on their location in the project.

How Propagation Works

Propagation is the mechanism that passes a LogRecord upward to parent loggers. Every logger has a boolean attribute named propagate, which defaults to True.

When a log event occurs on a child logger (e.g., child.warning("Query failed")), the process unfolds as follows:

  1. Child Level Check: The child logger checks its own threshold level. If the log message meets or exceeds this level, a LogRecord is created.
  2. Child Handlers Executed: The child logger passes the record to its own attached handlers (if any exist).
  3. Propagation Check: The logger checks its propagate attribute. If True, it passes the record directly to parent.handlers.
  4. Ascending the Tree: The record travels up the chain—from child, to parent, to root—triggering the handlers attached to each ancestor.

The Nuance of Ancestor Log Levels

A critical detail in Python's propagation model is that ancestor log levels are bypassed during propagation.

Once the emitting logger passes its own level check and creates a LogRecord, ancestor loggers do not re-evaluate their configured log levels. The ancestor loggers simply execute their attached handlers on the incoming record. Only filters attached to ancestor loggers or handlers can intercept the record once propagation begins.

For example, if app.db has a level of DEBUG and the root logger has a level of WARNING, calling app.db.debug("message") will still cause the root logger's handlers to process the record. The root logger's WARNING threshold applies only to events initiated directly on the root logger, not to propagated records.

Preventing Duplicate Logs

A common issue in Python logging is duplicate log lines. This usually happens when a developer attaches a handler (such as a StreamHandler printing to the console) to a child logger while the root logger also has a console handler attached.

Because propagation is enabled by default:

  1. The child's handler outputs the message.
  2. The record propagates to the root logger.
  3. The root's handler outputs the message a second time.

To stop a logger from passing its records up to its parents, set propagate to False:

logger = logging.getLogger("app.db")
logger.addHandler(logging.FileHandler("db.log"))
logger.propagate = False  # Records stop here and do not reach the root logger

Standard Pattern: Centralized Handlers

The intended design of the logging module is to configure handlers at the top level and emit records at the bottom:

  1. Attach formatters and handlers (console, files, remote services) exclusively to the root logger or a top-level application logger ("app").
  2. Use logging.getLogger(__name__) inside individual libraries and submodules without attaching handlers to them.
  3. Allow propagation to carry all records up to the central handlers.

This pattern keeps modular code decoupled from logging destinations while maintaining total visibility across the application.