Code of the Day
IntermediateScheduling and Configuration

Config and logging

Read a TOML config, override values with environment variables, and wire the result into Python's logging module — the complete pattern for observable, configurable scripts.

WorkflowIntermediate10 min read
Recommended first
By the end of this lesson you will be able to:
  • Read a TOML config with tomllib (or parse an inline TOML string)
  • Override config values with os.environ.get()
  • Configure Python's logging module with a level read from config
  • Distinguish INFO vs DEBUG log output in practice

Configuration and logging are two sides of the same coin for automation scripts. Configuration controls what the script does; logging tells you what it actually did. This lesson wires them together: read a TOML config, apply environment overrides, and configure Python's logging module from the result.

Python's logging module

logging is in the standard library and is the right tool for any script that runs unattended. Unlike print(), it:

  • Timestamps every message automatically
  • Filters messages by severity level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  • Can be redirected to a file, a remote service, or stderr without changing the code
  • Is safe to use in libraries without interfering with application log config

The minimal setup:

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)

logging.info("Script started")
logging.debug("This won't appear at INFO level")
logging.warning("Something looks wrong")

basicConfig() only takes effect the first time it is called — call it once, early, before any other logging calls.

Log levels

LevelUse for
DEBUGDetailed trace info for debugging
INFONormal progress messages
WARNINGSomething unexpected but non-fatal
ERRORA failure that requires attention
CRITICALSystem is unusable

In production, INFO is the standard level. During development or debugging, DEBUG gives you the full picture. The level from config controls which messages appear.

Reading the config and wiring logging

The pattern in full:

import os
import tomllib
import logging

# Inline TOML string (simulates reading from a file)
TOML_CONFIG = """
log_level = "INFO"
output_path = "/tmp/reports"
batch_size = 50
"""

# Parse the TOML
config = tomllib.loads(TOML_CONFIG)

# Apply environment overrides
if os.environ.get("LOG_LEVEL"):
    config["log_level"] = os.environ["LOG_LEVEL"]

# Configure logging from the resolved config
level = getattr(logging, config["log_level"].upper(), logging.INFO)
logging.basicConfig(
    level=level,
    format="%(asctime)s %(levelname)s %(message)s",
)

logging.info("Config loaded: output_path=%s, batch_size=%d",
             config["output_path"], config["batch_size"])
logging.debug("Full config: %s", config)

getattr(logging, "DEBUG") is the idiomatic way to convert a string level name to the integer constant. The fallback logging.INFO ensures a bad config value does not crash the script.

Use %s style formatting in logging calls rather than f-strings: logging.debug("value: %s", x) instead of logging.debug(f"value: {x}"). The reason: if the message is below the current log level, the %s substitution never happens — a small but real performance saving in hot paths.

Try it

The runner below shows the layered pattern in action: parse inline TOML, apply an environment override, configure logging, and see what different levels produce:

Python — editable, runs in your browser

Notice that the DEBUG message only appears after the level is changed to DEBUG. At INFO level it is silently filtered. This is how log levels work in practice: you write debug messages throughout your code and they are invisible in production, available when you need them.

Where to go next

Next: lab — scheduled job — build a complete script that reads a TOML config, fetches data from a public API, writes a JSON report, logs its activity, and shows how it would run on a schedule.

Finished reading? Mark it complete to track your progress.

On this page