logging.config.dictConfig centralizes Python logging in a dictionary. Instead of letting every module create handlers and formats, the application defines destinations, levels, and propagation once. This avoids duplicate records and makes development and production behavior easier to change.

This article builds on the Python logging guide. Libraries should only emit events through logging.getLogger(__name__); the application owns output configuration.

Minimal console configuration

import logging
from logging.config import dictConfig

LOGGING = {
    "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"},
}

dictConfig(LOGGING)
logger = logging.getLogger(__name__)
logger.info("application started")

version is required and currently must be 1. Setting disable_existing_loggers to False avoids unexpectedly silencing loggers created by dependencies before configuration. Logger and handler levels both filter records, so configure them deliberately.

Per-module loggers and propagation

"loggers": {
    "my_app.db": {
        "level": "WARNING",
        "handlers": [],
        "propagate": True,
    },
    "urllib3": {
        "level": "WARNING",
        "handlers": [],
        "propagate": True,
    },
}

A logger with propagate=True passes records to ancestors. If it also owns an equivalent handler, the same record may appear twice. Keep handlers on the root logger when all modules share destinations.

Rotate files instead of growing forever

"handlers": {
    "file": {
        "class": "logging.handlers.RotatingFileHandler",
        "filename": "logs/app.log",
        "maxBytes": 10_000_000,
        "backupCount": 5,
        "encoding": "utf-8",
        "formatter": "standard",
        "level": "INFO",
    },
}

Create the directory before applying configuration. Containers and managed platforms usually work better when applications write to the console and infrastructure collects and rotates records. Do not depend on a local file for durable audits without understanding deployment storage.

Add context without constructing strings

logger.info(
    "order processed",
    extra={"request_id": request_id, "order_id": order.id},
)

Add these names to the formatter when using standard logging. Ensure every record supplies them or use a filter that provides defaults, otherwise formatting will fail. Never log passwords, access tokens, session cookies, or unnecessary personal data.

Configure once and test

Call dictConfig in the entry point before workers start. Repeated reconfiguration makes behavior difficult to reason about. In tests, pytest and caplog can inspect records without relying on final file output; see testing logs with caplog.

Verify environment-specific levels, absence of duplicates, encoding, and rotation. Raise a controlled error and call logger.exception() inside except to confirm tracebacks. Ensure secrets are removed before a record reaches any handler.

The official logging.config reference, accessed July 28, 2026, defines the dictConfig schema and configuration errors.