Levels, Loggers, and Propagation
Levels, Loggers, and Propagation
The logging module uses a numeric severity scale — DEBUG 10, INFO 20, WARNING 30, ERROR 40, CRITICAL 50 — and a dot-separated logger hierarchy: app.api is a child of app, which is a child of the root logger.
import logging
log = logging.getLogger(__name__) # "package.module", not the root
def handle(req):
log.debug("payload: %s", req) # %s stays lazy — no work if DEBUG is off
if req.get("bad"):
log.error("rejected %s", req["id"])
A logger with NOTSET level inherits from its nearest ancestor. Messages also propagate up to ancestor handlers unless propagate = False. Effective level comes from the root logger by default, which is WARNING until you configure it.
Handlers, Formatters, and Rotation
Handlers, Formatters, and Rotation
Loggers decide whether to record; handlers decide where it goes:
import logging
from logging.handlers import RotatingFileHandler
root = logging.getLogger()
root.setLevel(logging.INFO)
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s: %(message)s")
console = logging.StreamHandler()
console.setFormatter(fmt)
fileh = RotatingFileHandler("app.log", maxBytes=5_000_000, backupCount=3,
encoding="utf-8")
fileh.setFormatter(fmt)
root.addHandler(fileh)
maxBytes + backupCount bound disk usage: the active file rolls to app.log.1, and the oldest backup is deleted. Libraries should only call logging.getLogger(...) and never install handlers — configuration belongs to the application entry point via basicConfig or dictConfig.
Structured Logs and Common Mistakes
Structured Logs and Common Mistakes
Human lines are fine for local dev; production wants machine-parseable events. Emit one JSON object per record with python-json-logger or structlog, adding context fields through extra=:
log.info("checkout.done", extra={"order_id": oid, "ms": elapsed})
Common failure modes:
print()bypasses levels, formatting, and streams — it disappears when stdout is redirectedlogging.basicConfig()is a no-op if handlers already exist, so "my INFO lines vanished" often means the root logger already had a WARNING handler- f-strings in log calls format eagerly even when the level is disabled — prefer
%sargs - Missing
encoding="utf-8"on FileHandler mangles non-ASCII under some locales
Quiz
1. What does logger = logging.getLogger("app.db") inherit when its own level is NOTSET?
2. Why should libraries avoid calling logging.basicConfig()?
3. Why prefer log.debug("val: %s", x) over log.debug(f"val: {x}")?
Flashcards
Question
Effective level
Click to reveal answer
Answer
A logger’s own level, or the nearest ancestor’s if NOTSET — root defaults to WARNING.
Question
Propagation
Click to reveal answer
Answer
Records bubble to ancestor loggers’ handlers; disable with propagate=False.
Question
RotatingFileHandler
Click to reveal answer
Answer
Rolls the file at maxBytes, keeping backupCount generations to cap disk use.
Question
Structured logging
Click to reveal answer
Answer
One JSON object per event with stable fields (order_id, ms) for querying in production.
Revision Notes
Key Takeaways
- 1. getLogger(__name__) per module; applications configure handlers once at entry point
- 2. Levels control filtering, handlers control destinations, propagation shares records up the tree
- 3. Use %-style lazy args and JSON output for anything shipped to production
Interview Tips
- • Explain the logger → handler → formatter pipeline and where each configuration decision belongs
- • Describe how you would correlate a request across services with structured fields
Cheat Sheet
levels 10/20/30/40/50; getLogger(name); %s lazy args; RotatingFileHandler(maxBytes, backupCount); JSON via extra/structlog; root default WARNING.