OSPython.036: Logging Configuration with dictConfig — Handlers, Formatters, Loggers, and JSON

Diverse software engineering team working with Python logging and monitoring in a modern server room

Python’s logging system becomes easier to maintain when configuration is separated from application logic. logging.config.dictConfig() lets you define formatters, handlers, logger levels, propagation rules, and output destinations in one structured dictionary instead of scattering setup code across a program.

This lesson continues the logging sequence after OSPython.033: Logging Filters, OSPython.034: Contextual Logging, and OSPython.035: Logging Exceptions. The goal now is to configure the whole logging graph in one place.

Learning Objectives

  • Explain what dictConfig() configures.
  • Build formatter, handler, and logger sections correctly.
  • Understand the required version key.
  • Use disable_existing_loggers deliberately.
  • Route INFO and ERROR records to different handlers.
  • Load logging configuration from JSON safely.
  • Verify the active configuration with real log records.

Why Move Beyond basicConfig()

logging.basicConfig() is useful for small scripts, but larger applications often need multiple handlers, different levels, file output, structured formatting, and separate logger namespaces. Writing all of that imperatively works, but it can become repetitive and difficult to audit.

Programming code displayed on a laptop screen
Programming code on a laptop. Photo by Negative Space, public domain/CC0 via Wikimedia Commons.

dictConfig() treats logging setup as data. That makes the configuration easier to inspect, test, reuse, serialize, and replace for different environments such as local development, staging, and production.

The Smallest Useful dictConfig

import logging
import logging.config

CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "standard",
            "level": "INFO",
        }
    },
    "root": {
        "handlers": ["console"],
        "level": "INFO",
    },
}

logging.config.dictConfig(CONFIG)
logger = logging.getLogger(__name__)
logger.info("Logging is configured")

The version key is required by the configuration schema. Today the documented value is 1. The formatter is named standard, the handler is named console, and the root logger sends records to that handler at INFO and above.

mCoding’s modern Python logging tutorial explains dictConfig, handlers, formatters, structured configuration, and production logging patterns.

Formatters Define Record Layout

The formatters section defines how a LogRecord becomes readable output. A formatter can include timestamps, levels, logger names, process IDs, filenames, line numbers, or custom contextual fields. Formatter names are local configuration identifiers; handlers refer to them by those names.

"formatters": {
    "detailed": {
        "format": (
            "%(asctime)s %(levelname)s %(name)s "
            "%(filename)s:%(lineno)d %(message)s"
        )
    }
}

Handlers Decide Where Records Go

Handlers determine the destination. A StreamHandler can write to standard output or standard error. A FileHandler writes to a file. Rotating handlers from earlier lessons can also be referenced by class name inside a dictionary configuration.

"handlers": {
    "console": {
        "class": "logging.StreamHandler",
        "formatter": "detailed",
        "level": "INFO",
        "stream": "ext://sys.stdout",
    },
    "errors": {
        "class": "logging.FileHandler",
        "formatter": "detailed",
        "level": "ERROR",
        "filename": "errors.log",
        "encoding": "utf-8",
    },
}

The special ext://sys.stdout syntax tells the logging configuration system to resolve an existing Python object instead of treating the value as an ordinary string.

Loggers Connect Names to Handlers

The loggers section controls named logger namespaces. A service can give app.api and app.worker different handlers or levels while still sharing common formatting. Logger hierarchy still applies, so propagation must be deliberate.

"loggers": {
    "app.api": {
        "handlers": ["console", "errors"],
        "level": "DEBUG",
        "propagate": False,
    }
}

Setting propagate to False prevents a record handled by app.api from also traveling upward to ancestor handlers and appearing twice. Duplicate output is one of the most common signs that logger hierarchy was configured without checking propagation.

Teclado explains the relationship between Python loggers, handlers, and formatters—the same objects dictConfig connects declaratively.

Use disable_existing_loggers Carefully

disable_existing_loggers deserves deliberate attention. If set to True, loggers that already exist and are not explicitly covered by the new configuration may be disabled. That can unexpectedly silence messages from libraries. Setting it to False is often safer when an application depends on third-party packages whose loggers should continue to operate.

Load Configuration From JSON

A dictionary can be created directly in Python, but it can also come from JSON because JSON objects map naturally to Python dictionaries. This allows operations teams to change levels or destinations without editing application code.

import json
import logging.config
from pathlib import Path

config_path = Path("logging.json")
with config_path.open("r", encoding="utf-8") as handle:
    config = json.load(handle)

logging.config.dictConfig(config)

Do not treat external configuration as automatically trustworthy. Validate file location, deployment ownership, expected keys, handler destinations, and permissions. A logging configuration can control file paths and importable classes, so configuration changes deserve the same operational discipline as other application settings.

Related social discussion: Emily Riederer recommends the mCoding logging tutorial for its modern mental model, best practices, and customization.

Production Example: Console Plus Error File

CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "standard": {
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s"
        }
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "standard",
            "level": "INFO",
            "stream": "ext://sys.stdout",
        },
        "errors": {
            "class": "logging.FileHandler",
            "formatter": "standard",
            "level": "ERROR",
            "filename": "errors.log",
            "encoding": "utf-8",
        },
    },
    "root": {
        "handlers": ["console", "errors"],
        "level": "DEBUG",
    },
}

With this setup, INFO and WARNING records appear on the console. ERROR and CRITICAL records appear on the console and are also written to errors.log. DEBUG records pass the root logger’s level test but are rejected by both handlers because their handler thresholds are higher. Logger level and handler level are separate gates.

Verification Checklist

  1. Emit one DEBUG record and confirm whether any handler accepts it.
  2. Emit one INFO record and verify the console output.
  3. Emit one ERROR record and verify both console and file output.
  4. Check that the formatter includes the expected timestamp, level, logger name, and message.
  5. Create a child logger and verify whether propagation causes duplicate output.
  6. Temporarily change a handler level and confirm that routing changes as expected.
  7. Restart the program and verify that the configuration is loaded consistently.

Common Failure Modes

  • Unknown formatter name: a handler references a formatter key that does not exist.
  • Unknown handler name: a logger references a handler key that does not exist.
  • Wrong class path: the configured handler class cannot be imported.
  • Duplicate records: a child logger handles a record and then propagates it to an ancestor with another handler.
  • Silent libraries: disable_existing_loggers=True disables loggers you expected to keep.
  • Missing file permissions: a file handler cannot open or create its target file.

Hands-On Lab

  1. Create a new Python file and import logging and logging.config.
  2. Define one formatter named standard.
  3. Create a console handler at INFO.
  4. Create a file handler at ERROR.
  5. Configure the root logger at DEBUG.
  6. Emit DEBUG, INFO, WARNING, ERROR, and CRITICAL records.
  7. Explain why each record does or does not appear in each destination.
  8. Move the dictionary into a JSON file and load it with json.load().
  9. Create a named logger with propagate=False and confirm that duplicate messages disappear.

Knowledge Check

  1. What module contains dictConfig()?
  2. What value is normally used for the required version key?
  3. What is the difference between a formatter and a handler?
  4. Why can propagate=True produce duplicate messages?
  5. Why can disable_existing_loggers=True be risky?
  6. Can JSON be used as a source for a logging configuration dictionary?
  7. If the root logger is DEBUG but a handler is INFO, will a DEBUG record reach that handler?

Answer Guide

  1. logging.config.
  2. 1.
  3. A formatter defines record layout; a handler decides where records are emitted and can impose its own level.
  4. The child logger’s handlers may emit the record and then ancestor handlers can emit the same record again.
  5. It may disable third-party or previously created loggers that are not explicitly named in the configuration.
  6. Yes. JSON objects can be loaded into Python dictionaries and passed to dictConfig().
  7. No. The handler rejects it because DEBUG is below the handler’s INFO threshold.

Key Takeaway

dictConfig() turns logging setup into a structured configuration graph. Formatters define representation, handlers define destinations, loggers define namespaces and routing, and levels determine which records survive each stage.

References

Primary references: Python documentation — Logging Configuration and Python Logging Cookbook.


BitcoinVersus.Tech

Advertisement

BitcoinVersus.Tech Editor’s Note:

We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support our independent technical education work, please donate Bitcoin here: 3C9o19EH5HSiwEPyCTmEKzxhNCbo2X6TTb

BitcoinVersus.tech is not a financial advisor. This lesson is for informational and educational purposes.

Leave a Reply