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.

:)
TABLE OF CONTENTS
6.8Production logging with the stdlib

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

You call logging.info("done") and the message appears. But when it does not appear, the hard question is: where in the pipeline did it get lost? Loggers, handlers, filters, and formatters form a chain, and a record only makes it to your output if every link cooperates.

Think of the logging module like a postal sorting facility. The logger is the postal worker who decides whether a letter needs processing. The filter is the sorting machine that discards junk mail. The handler is the delivery truck that transports the letter. The formatter is the label printer that stamps the destination. One broken link and the letter never arrives.

Core answer

Application modules should usually get a named logger and emit stable messages plus safe contextual fields. Libraries should not call basicConfig() as a side effect because configuration belongs to the application boundary.

# [CURRENT - 3.10-3.14] Works on Python 3.x
import logging
log = logging.getLogger(__name__)
def charge(order_id, amount):
log.info("charging order", extra={"order_id": order_id, "amount": str(amount)})

Logger names form a hierarchy. a.b.c is a child of a.b, which is a child of a. That hierarchy is what lets a single configuration govern a whole subsystem.

Mechanism and propagation

A logging call first checks whether the logger is enabled for that level. If it is, a LogRecord is created, then passed through logger filtering, handler filtering, formatting, and emission. Unless propagation is disabled, the record walks up the logger hierarchy toward parent handlers.

# [CURRENT - 3.10-3.14] Works on Python 3.x
import logging
logger = logging.getLogger("a.b")
print(logger.propagate)
print(logger.isEnabledFor(logging.INFO))

That isEnabledFor gate is why %-style argument logging matters. With:

# [CURRENT - 3.10-3.14] Works on Python 3.x
import logging
log = logging.getLogger(__name__)
row_count = 250
source = "orders.csv"
log.debug("loaded %s rows from %s", row_count, source)

the expensive final string formatting is deferred until the message is actually emitted. By contrast, an f-string is formatted before the logging call even if the level is disabled.

Exceptions and record shape

Use logger.exception() inside an exception handler to include traceback information. It is effectively error(..., exc_info=True).

# [CURRENT - 3.10-3.14] Works on Python 3.x
import logging
class PaymentError(Exception):
pass
class Order:
def __init__(self, order_id):
self.id = order_id
def process_order(order):
raise PaymentError("card declined")
log = logging.getLogger(__name__)
order = Order(order_id="A-1042")
try:
process_order(order)
except PaymentError:
log.exception("payment processing failed", extra={"order_id": order.id})
print("surface domain failure to caller")

The extra mapping adds attributes to the LogRecord. That is powerful, but it also means names can collide with standard record attributes if you choose them carelessly.

Version context

logging is stable standard library across Python 3. Current project guidance targets Python 3.10-3.14. Python 3.9 and below are End-of-Life.

The stdlib does not impose a structured logging schema. JSON output or downstream ingestion structure is typically handled by custom formatters or external log pipelines.

Edge cases and gotchas

Never log secrets, raw tokens, passwords, full request bodies, or unnecessary personal data. Logs are durable operational data, not a safe debugging scratchpad.

# [CURRENT - 3.10-3.14] Works on Python 3.x
import logging
class Order:
def __init__(self, order_id, customer_id):
self.id = order_id
self.customer_id = customer_id
log = logging.getLogger(__name__)
order = Order(order_id="A-1042", customer_id="C-88")
safe = {
"order_id": order.id,
"customer_id": order.customer_id,
}
log.info("order accepted", extra=safe)

Logging can also fail operationally:

  • slow handlers can stall latency-sensitive paths
  • full disks can block file handlers
  • recursive logging from inside handlers can create feedback loops
  • noisy debug logs can dominate cost and signal quality

Logs are production data. Treat them as retained, searchable, and potentially visible to more people and systems than the application itself.

Production usage

Configure logging once at the application entry point. Use module loggers everywhere else. Use levels consistently:

  • debug for deep diagnostics
  • info for expected lifecycle events
  • warning for degraded but continuing behavior
  • error for failed operations
  • critical for process- or system-level failure
# [CURRENT - 3.10-3.14] Works on Python 3.x
import logging
def configure_logging():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)

Pair logging with explicit boundary parsing and type clarity so contextual fields are predictable and searchable. See . For deterministic cleanup around handlers and files, use context managers and explicit teardown rather than garbage-collection timing; see .

Further depth
  • logging module
  • Logging HOWTO
  • Logging Cookbook
  • LogRecord attributes
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