Logging
print() cannot be turned down, filtered or routed. Run a delivery script twice, at INFO and at WARNING, and watch which messages survive when one setting changes.
When a script is small, print() is fine. When the script becomes a service that runs all day, print() starts to hurt: you cannot turn the chatter down without editing code, you cannot tell a warning from a routine message, and you cannot send some messages to a file and others to a dashboard. Python’s logging module fixes all three.
Every log message has a level. From quietest to loudest: DEBUG (10), INFO (20), WARNING (30), ERROR (40), CRITICAL (50). You set one threshold, and only messages at that level or above are written. The default threshold is WARNING, which is why a bare logging.info(...) in a fresh script prints nothing.
The program below tracks one food delivery order. It logs at four levels, then ends with a print(). Press Run to see it at level INFO. Then switch Try it to the WARNING run and watch two lines disappear without a single code change.
# Track a food delivery order with the logging module.
# Run as: python3 delivery.py (INFO and above)
# python3 delivery.py WARNING (WARNING and above)
import logging
import sys
level_name = sys.argv[1] if len(sys.argv) > 1 else "INFO"
logging.basicConfig(level=level_name, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger("delivery")
order = {"id": "OD-4471", "items": 3, "distance_km": 6.5}
log.debug("Raw order: %s", order)
log.info("Order %s accepted with %d items", order["id"], order["items"])
if order["distance_km"] > 5:
log.warning("Order %s is %.1f km away, may be late", order["id"], order["distance_km"])
rider = None
if rider is None:
log.error("No rider available for order %s", order["id"])
print("Done. print() goes to stdout, logging goes to stderr.")Press Play to run the program line by line, or step with ← →.
Variables
Nothing defined yet.
Output
Nothing printed yet.
What to notice
- One setting decides what you see.
basicConfig(level=...)is called once, at the top. Everything below that level is dropped. Changing INFO to WARNING removed the INFO line and the program did not change. - Named loggers tell you who spoke.
logging.getLogger("delivery")putsdeliveryin every line. In real modules the convention islogging.getLogger(__name__), so the logger name is the module path. - The format is a template.
%(levelname)s %(name)s: %(message)spicks fields from each record. Add%(asctime)sfor a timestamp. The default format, if you give none, islevelname:name:message. - Placeholders are filled in late.
log.info("Order %s", order_id)passes the value separately. Logging only builds the string if the message is going to be written. Dropped DEBUG lines are nearly free. - Logging an error is not raising one.
log.error(...)records the problem and carries on. If the program must stop, raise an exception as well. - stdout and stderr are different streams.
print()goes to stdout. logging’s default handler writes to stderr. A server can route them separately, which you cannot do with twoprint()calls.
basicConfig() before the first log message. If the root logger already has handlers, a later basicConfig() does nothing unless you pass force=True.Why this matters for AI work
An agent in production makes model calls, runs tools and keeps state, and most of it happens where you cannot see it. ADK uses this same logging module: its loggers are named google_adk. plus the module path, so logging.getLogger("google_adk").setLevel(logging.DEBUG) shows every model request, and raising it back to WARNING keeps only problems. The Logging and metrics lesson builds on exactly what you just saw.