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.
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.ximport logginglog = 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.
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.ximport logginglogger = 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.ximport logginglog = logging.getLogger(__name__)row_count = 250source = "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.
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.ximport loggingclass PaymentError(Exception): passclass Order: def __init__(self, order_id): self.id = order_iddef 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.
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.
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.ximport loggingclass Order: def __init__(self, order_id, customer_id): self.id = order_id self.customer_id = customer_idlog = 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.
Configure logging once at the application entry point. Use module loggers everywhere else. Use levels consistently:
debugfor deep diagnosticsinfofor expected lifecycle eventswarningfor degraded but continuing behaviorerrorfor failed operationscriticalfor process- or system-level failure
# [CURRENT - 3.10-3.14] Works on Python 3.ximport loggingdef 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 .