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=Trueattaches 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.