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.

8 min Free

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.

Ready
# 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 ← →.

00/11

Variables

Nothing defined yet.

Output

Nothing printed yet.

What to notice

  1. 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.
  2. Named loggers tell you who spoke. logging.getLogger("delivery") puts delivery in every line. In real modules the convention is logging.getLogger(__name__), so the logger name is the module path.
  3. The format is a template. %(levelname)s %(name)s: %(message)s picks fields from each record. Add %(asctime)s for a timestamp. The default format, if you give none, is levelname:name:message.
  4. 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.
  5. 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.
  6. 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 two print() calls.

Tip
Call 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.

Check your understanding

1. A fresh script calls logging.info('started') with no basicConfig. What appears?
2. Why write log.info('Order %s', order_id) instead of log.info(f'Order {order_id}')?
3. Your service prints with print() and is too noisy in production. What is the cheapest fix?