Production logging with the stdlib

Use logger hierarchy, levels, extra context, and safe exception logging

`print()` works until you need levels, routing, or structured output. Python's standard `logging` module provides hierarchical loggers, configurable levels (`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`), multiple handlers (console, file, rotating, HTTP), and formatters. The `extra` parameter attaches structured context to log records. `logger.exception()` is equivalent to `logger.error(exc_info=True)`, logging the full traceback. Lazy formatting with `%s` style avoids string construction when the log level discards the message. Loggers follow a parent-child hierarchy: child loggers propagate to parents by default. This page covers configuration patterns, context enrichment, and safe exception logging. <a href="/async-context-backpressure">Apply logging patterns to async services</a>. <a href="/async-servers-services">Add logging to async server handlers</a>.

Understand.
Visualize.
Master.

Python in Depth

An interactive engineering reference for Python internals

Quick note

Logs should help operators decide.

:)
Python version

Targets Python 3.10–3.14. Python 3.9 and below are End-of-Life.

TABLE OF CONTENTS
6.8Production logging with the stdlib

Use logger hierarchy, levels, extra context, and safe exception logging

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 logging
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Charge:
order_id: str
amount_cents: int
def 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 logging
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Order:
order_id: str
tenant: str
def 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 logging
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class ImportBatch:
source: str
rows: int
def 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.

Further reading

  • Official docs: logging
  • Official docs: logging HOWTO
  • Official docs: logging cookbook
  • PEP 282: logging system
  • CPython source: logging package
BOARD NOTESContext
WHY NO BENCHMARK?

This topic is better taught with structure, semantics, and cross-references than with a synthetic chart.

Logs should help operators decide.

RELATED GUIDES
NEXT CHECKS
Contribute