Skip to main content

Logging That Operators Read

intermediate13 min readLesson 78 of 169

Logger hierarchy, levels, lazy formatting, and why print is not logging.

print goes to stdout and dies there. The logging module routes events to handlers, formats them uniformly, and carries levels for severity:

import logging

log = logging.getLogger(__name__)          # one logger per module

log.debug("cache size: %d", len(cache))    # detail for developers
log.info("imported %d rows", count)        # normal milestones
log.warning("retrying request (%d/3)", attempt)
log.error("failed to save %s", task_id, exc_info=True)   # + traceback

Professional habits baked into that snippet:

  • getLogger(__name__) names loggers after modules (app.storage), so operators can silence or amplify each area independently.
  • Lazy % formatting, not f-strings โ€” the string is only built if the level is enabled.
  • exc_info=True attaches the traceback to error logs; losing it is how "it worked locally" mysteries are born.

Levels are a contract

DEBUG โ†’ INFO โ†’ WARNING โ†’ ERROR โ†’ CRITICAL. Choose by who must react: developers tune DEBUG out in production; INFO is the operational heartbeat; WARNING means degraded-but-working; ERROR means an operation failed. Emitting everything as INFO is how logs become unreadable.

Configuration belongs to the entry point

Libraries only get loggers; the application configures them (logging.basicConfig(level=..., format=...)) in one place โ€” usually __main__. That split is why a library can never spam a host app's console.

Now practice

Logging DrillsLog with the right logger, level, and lazy formatting.2 challenges ยท ยท ~20 min