The stdlib logging package is a pipeline: logger, record, filters, handlers, formatters, and propagation. Treat it as operational infrastructure, not a fancier print.
Core answer
Application entry points configure logging. Modules obtain named loggers and emit stable messages with safe context. Use lazy formatting so disabled levels avoid string work.
# [CURRENT - 3.10-3.14] Works on Python 3.10+import loggingfrom dataclasses import dataclass@dataclass(frozen=True, slots=True)class Charge: order_id: str amount_cents: intdef charge(log: logging.Logger, payment: Charge) -> None: log.info("charging order %s for %s cents", payment.order_id, payment.amount_cents)logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s %(message)s")logger = logging.getLogger("billing")charge(logger, Charge("ORD-7", 4200))Why this design exists
PEP 282 standardized a configurable logging system because libraries and applications need routing, levels, hierarchy, and formatting without hard-coded output policy.
Mechanics and CPython internals
A logger checks level, builds a LogRecord, applies filters, hands records to handlers, and propagates to parents unless propagation is disabled. Handler I/O can be slow or blocking. The module is thread-safe at its own coordination points, but that does not make arbitrary contextual data safe to leak.
# [CURRENT - 3.10-3.14] Works on Python 3.10+import loggingfrom dataclasses import dataclass@dataclass(frozen=True, slots=True)class Order: order_id: str tenant: strdef log_accept(order: Order) -> None: base = logging.getLogger("orders.accept") adapter = logging.LoggerAdapter(base, {"tenant": order.tenant}) adapter.info("accepted %s", order.order_id)logging.basicConfig(level=logging.INFO, format="%(levelname)s %(tenant)s %(message)s")log_accept(Order("ORD-7", "TEN-1"))Complexity and tradeoffs
Disabled logger levels avoid much record work when logging is written idiomatically. F-strings still format before the level gate. Rich context improves diagnostics, while secret and high-cardinality fields create retention, privacy, and cost problems.
Idiomatic patterns and refactoring
Refactor library-side configuration and eager formatting into named loggers and lazy message arguments.
# [CURRENT - 3.10-3.14] Works on Python 3.10+import loggingfrom dataclasses import dataclass@dataclass(frozen=True, slots=True)class ImportBatch: source: str rows: intdef log_bad(log: logging.Logger, batch: ImportBatch) -> None: log.debug(f"loaded {batch.rows} rows from {batch.source}")def log_batch(log: logging.Logger, batch: ImportBatch) -> None: log.debug("loaded %s rows from %s", batch.rows, batch.source)logger = logging.getLogger(__name__)logger.setLevel(logging.INFO)log_bad(logger, ImportBatch("orders.csv", 200))log_batch(logger, ImportBatch("orders.csv", 200))Common mistakes and edge cases
Do not call basicConfig from reusable libraries. Do not log raw secrets, tokens, passwords, or unnecessary personal data. Use logger.exception inside an exception handler when the traceback belongs in the record.
When to use / When NOT to use
Use logging for operational events, diagnostics, error context, and routing records to configured sinks.
Do not use logs as a durable business ledger, a metrics system, or a dumping ground for unbounded request data.