Use a configured logger with levels, handlers, and structured context instead of print
print writes to stdout and cannot be filtered, routed, or enriched. The logging module gives you a hierarchy of named loggers, level filtering, multiple handlers (console, file, syslog, JSON), and structured formatting that includes timestamp, level, logger name, and message. Use logger = logging.getLogger(name) so the logger name reflects module structure and can be configured per package. Configure handlers once at the entry point; library code should only create loggers and call methods. Use the lazy % formatting in log calls rather than f-strings so that string construction is skipped when the level is disabled. For production services, emit JSON to stdout and let the platform collect it, and include a correlation or request id via LoggerAdapter or a filter.
Levels: DEBUG for development detail, INFO for lifecycle events, WARNING for recoverable anomalies, ERROR for failed operations, CRITICAL for process-threatening failures.
Use logging.getLogger(name) in every module; never use the root logger directly in libraries.
Call logging.basicConfig or dictConfig once at the entry point, not in libraries.
Use logger.error('...', exc_info=True) or logger.exception(...) inside except to preserve the traceback.
Prefer lazy logging: logger.info('user %s did %s', user_id, action) over f-strings, since the format is skipped when the level is off.
Common mistake: calling logging.info at module import time before configuration, which uses the last-resort handler and prints unexpectedly.
Common mistake: logging inside a hot loop without a level check, which can dominate runtime.
Version note: dictConfig and structlog-style JSON logging are widely used in 3.2+. The default lastResort handler behavior has been stable for years.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience