The logging Module
The stdlib logging package routes log records from loggers through handlers to outputs (console, files, syslog). Configure levels, formatters, and logger hierarchy once at startup - libraries log to named loggers without configuring handlers.
Recipe
import logging
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
log = logging.getLogger(__name__)
log.info("server starting")
log.warning("disk %s%% full", 92)When to reach for this:
- Production services need leveled, filterable diagnostics
- Libraries emit logs without choosing output destination
- Multiple handlers (stderr + rotating file) per logger
- Structured fields later map from LogRecord attributes
- Replacing print debugging with operable trails
Working Example
import logging
from logging.handlers import RotatingFileHandler
def configure_logging() -> None:
root = logging.getLogger()
root.setLevel(logging.INFO)
fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
console = logging.StreamHandler()
console.setFormatter(fmt)
root.addHandler(console)
file_handler = RotatingFileHandler("app.log", maxBytes=1_000_000, backupCount=3)
file_handler.setFormatter(fmt)
root.addHandler(file_handler)
configure_logging()
log = logging.getLogger("billing")
log.info("charge processed id=%s", "inv-1")
try:
1 / 0
except ZeroDivisionError:
log.exception("unexpected math failure")What this demonstrates:
basicConfigalternative: explicit handler attachment on root- Named loggers inherit root level and handlers
- Lazy
%formatting - arguments deferred if level filtered log.exceptionincludes traceback when called insideexcept
Deep Dive
How It Works
- Loggers form hierarchy by dotted names (
app.api,app.db) - Records flow: logger -> filters -> handlers -> formatters -> IO
- Levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
propagatecontrols bubbling to parent loggers- Libraries call
getLogger(__name__)only - no handler setup
Components
| Piece | Role |
|---|---|
| Logger | Entry point log.info(...) |
| Handler | Output sink |
| Formatter | Layout string |
| Filter | Extra gating |
Gotchas
- basicConfig no-op if root configured - second call ignored. Fix: configure once in main or use
dictConfig. - Duplicate logs from propagate - child and root both have handlers. Fix: set
logger.propagate = Falseor handlers only on root. - f-strings in log calls - eager evaluation even when filtered. Fix:
log.info("x=%s", x). - print in libraries - bypasses levels and aggregation. Fix: logging only.
- DEBUG in production always on - volume and secret leakage. Fix: env-driven level, sample debug.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| structlog | JSON pipelines, context vars | Tiny scripts |
| OpenTelemetry logs | Unified traces/metrics/logs | No collector yet |
| REPL exploration | Services |
FAQs
Where do I configure logging?
Application entry (main, FastAPI lifespan, Django LOGGING dict) - never in library modules.
What logger name should I use?
logging.getLogger(__name__) so names mirror package structure.
How do I log exceptions?
log.exception("msg") inside except block, or log.error("msg", exc_info=True).
dictConfig vs basicConfig?
dictConfig scales for multiple handlers/formatters and test overrides.
How do I silence noisy libraries?
logging.getLogger("urllib3").setLevel(logging.WARNING).
Are thread-safe loggers?
Yes - logging module serializes handler emit; still avoid mutable context without locks.
How do rotating files work?
RotatingFileHandler by size; TimedRotatingFileHandler by interval - pair with log shipping.
Should I use LoggerAdapter?
For static context (request id prefix) before structured logging libs.
What about log levels in tests?
caplog fixture in pytest captures records by logger name and level.
How does this compare to Django logging?
Django uses dictConfig under the hood - same concepts, framework-provided defaults.
Related
- Structured & Contextual Logging - JSON and correlation IDs
- Warnings & Deprecations - separate from logging
- Exceptions Basics - log and raise
- Errors & Logging Best Practices - policies
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.