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.
- 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
| Level | Use for |
|---|---|
| DEBUG | Detailed trace info for debugging |
| INFO | Normal progress messages |
| WARNING | Something unexpected but non-fatal |
| ERROR | A failure that requires attention |
| CRITICAL | System 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:
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.
Config files
Separating configuration from code lets you change a script's behaviour without editing source — understand the common formats and the layered override pattern that makes scripts flexible and safe.
Lab: Scheduled job
Build a configured, logged script that fetches data from a public API, writes a JSON report, and is wired to run on a schedule — end-to-end practice for the scheduling and configuration module.