Logging in Python¶
Logging is how a running program explains itself. Done well, logs make bugs reproducible, production issues diagnosable, and normal operation observable without scattering
print()everywhere.
Why Logging Instead of print¶
| Concern | print() |
logging |
|---|---|---|
| Level control | no built-in way to silence selectively | levels let you filter noise |
| Destination | stdout only | files, streams, network, handlers |
| Formatting | manual | formatters centralize structure |
| Production safety | easy to leak noisy output | configurable per environment |
| Context | ad-hoc | easy to attach consistent metadata |
Use print() for quick probes. Use logging when you want durable, filterable diagnostics.
Quick Reference¶
| Level | Typical use |
|---|---|
DEBUG |
Detailed diagnostic info, internal state |
INFO |
Normal operational events — started, finished, key milestones |
WARNING |
Something unexpected but not a failure |
ERROR |
A problem that prevented part of the work |
CRITICAL |
Severe failure; the application may not continue |
The Smallest Working Logger¶
import logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
def scale(values):
logger.debug("scale input=%r", values)
mean = sum(values) / len(values)
logger.info("mean computed=%r", mean)
return [v - mean for v in values]
__name__ gives each module its own logger name, which is the standard convention.
Log Levels in Practice¶
logger.debug("cache key=%s, hit=%s", key, hit)
logger.info("processing batch id=%s size=%d", batch_id, len(items))
logger.warning("fallback used, primary service unavailable")
logger.error("failed to write result: %s", exc_info=True)
DEBUGis for details you usually do not want in production.INFOdescribes progress and meaningful milestones.WARNINGis for recoverable oddities.ERRORsignals a failure of some operation.CRITICALis for fatal application-level problems.
Do not use INFO as a dump for everything. If every step is INFO, nothing stands out.
basicConfig¶
For simple programs and scripts, basicConfig is enough.
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)-8s %(name)s %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
)
Call basicConfig once, early. It configures the root logger.
Logger, Handler, Formatter¶
The logging system has three layers:
- Logger — the object you call (
logger.info(...)). - Handler — where the log record goes (stream, file, etc.).
- Formatter — how the record is rendered as text.
import logging
logger = logging.getLogger("analysis")
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()
handler.setLevel(logging.INFO)
formatter = logging.Formatter(
"%(asctime)s %(levelname)-8s %(name)s %(message)s"
)
handler.setFormatter(formatter)
logger.addHandler(handler)
Log records flow from logger to handler to formatter. A logger can have multiple handlers, each with its own level and format.
Common Handlers¶
| Handler | Use for |
|---|---|
StreamHandler |
console / stdout or stderr |
FileHandler |
text file output |
RotatingFileHandler |
file with size-based rotation |
TimedRotatingFileHandler |
file with time-based rotation |
NullHandler |
silence logging (for library default) |
from logging.handlers import RotatingFileHandler
file_handler = RotatingFileHandler(
"app.log", maxBytes=10_000_000, backupCount=5
)
file_handler.setFormatter(logging.Formatter("%(asctime)s %(message)s"))
logger.addHandler(file_handler)
Configuration Styles¶
Programmatic¶
Good for applications where you want explicit control in code.
import logging
def setup_logging():
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s %(message)s",
)
INI-style (fileConfig)¶
logging.ini:
[loggers]
keys=root,analysis
[handlers]
keys=console
[formatters]
keys=standard
[logger_root]
level=INFO
handlers=console
[logger_analysis]
level=DEBUG
handlers=console
qualname=analysis
propagate=0
[handler_console]
class=StreamHandler
level=INFO
formatter=standard
args=(sys.stdout,)
[formatter_standard]
format=%(asctime)s %(levelname)-8s %(name)s %(message)s
datefmt=%Y-%m-%d %H:%M:%S
DictConfig¶
For more complex setups, especially in web apps and services:
import logging.config
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)-8s %(name)s %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
"stream": "ext://sys.stdout",
}
},
"loggers": {
"analysis": {
"level": "DEBUG",
"handlers": ["console"],
"propagate": False,
}
},
"root": {
"level": "INFO",
"handlers": ["console"],
},
}
logging.config.dictConfig(LOGGING)
Structured Logging¶
Pure text logs are hard to query and hard to parse reliably. Structured logging emits key-value pairs so downstream tools can filter and aggregate.
Simple structured style¶
logger.info(
"batch complete",
extra={"batch_id": batch_id, "rows": len(rows), "duration_s": duration}
)
Be cautious: extra requires the formatter and filters to handle unknown keys gracefully. Many projects use a structured logging library instead.
JSON logging¶
For machine-readable logs:
import json
import logging
class JsonFormatter(logging.Formatter):
def format(self, record):
payload = {
"time": self.formatTime(record),
"level": record.levelname,
"name": record.name,
"message": record.getMessage(),
}
if record.exc_info and record.exc_info[1]:
payload["exception"] = str(record.exc_info[1])
return json.dumps(payload)
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)
JSON logs are easier to index, filter, and ship to log aggregation systems.
Logging Exceptions¶
try:
result = risky_operation()
except Exception as e:
logger.error("risky_operation failed: %s", e, exc_info=True)
result = fallback()
exc_info=True includes the traceback in the log record. This is the difference between "something failed" and "here is exactly why it failed."
Avoid bare except: that swallows exceptions silently.
Logging in Libraries vs Applications¶
- Libraries should use
getLogger(__name__)and avoid configuring handlers. Let the application decide how logs are handled. - Applications configure handlers, levels, and formatters.
- Library code should usually not call
basicConfig.
A library's logger should default to NullHandler when no configuration is present, so it does not emit unexpected output.
Context and Correlation¶
For request-driven or batch processing systems, attaching context makes logs far more useful.
The exact mechanism depends on your framework. Many use logging filters or context variables to inject request-scoped metadata automatically.
Good context fields: request id, trace id, user id, job id, batch id, node, environment.
What to Log¶
Log for: - Startup and shutdown milestones. - Significant state transitions. - Recoverable problems and fallback behavior. - Failures with enough context to reproduce. - Key inputs and outputs at boundaries, when useful. - Performance-relevant operations when they matter.
Avoid logging: - Secrets, tokens, passwords, keys. - Full payloads when they are large or sensitive. - High-frequency per-item debug spam in production without a clear reason. - Personal data unless you have a specific, compliant reason.
Configuration by Environment¶
A common pattern:
| Environment | Root level | App logger | Notes |
|---|---|---|---|
| Development | DEBUG | DEBUG | verbose, local console |
| Staging | INFO | INFO or DEBUG | more signal, less noise |
| Production | INFO or WARNING | INFO | ERROR/CRITICAL for alerts |
Set the level via environment variable or configuration, not by editing code per environment.
import os
import logging
level = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(level=getattr(logging, level, logging.INFO))
Log Aggregation Basics¶
In larger systems, logs from many processes need to be searchable. Common goals: - Centralized collection (files, agents, sockets). - Structured format (JSON or key-value) for filtering. - Correlation via request/job ids. - Retention and rotation policies.
The logging module is the source; aggregation is the destination. Keep them decoupled.
Performance Notes¶
- Loggers are cheap when the level is not enabled, because the message is not formatted.
- Avoid expensive computation inside a log call if the level may be disabled.
# Avoid heavy work at DEBUG when DEBUG may be off
if logger.isEnabledFor(logging.DEBUG):
logger.debug("stats=%s", expensive_stats())
- Use
%-style formatting in the logging call, not f-strings, when the args may be skipped. The logging framework handles formatting only if the record is emitted.
Common Mistakes¶
- Logging everything at DEBUG in production — too much noise, too much cost.
- Swallowing exceptions silently — use
ERRORwithexc_info=True. - Logging secrets — tokens, keys, passwords should never appear in logs.
- Using print for operational signals — you lose filtering and structure.
- Inconsistent logger names —
__name__is the easy convention. - Configuring handlers in library code — leave that to the application.
- No correlation context — hard to trace one request across many log lines.
- Overusing CRITICAL — reserve it for truly fatal situations.
Practical Example — Logging a Data Pipeline Step¶
import logging
import time
logger = logging.getLogger("pipeline")
def clean_and_load(path, batch_id):
logger.info("step=start", extra={"batch_id": batch_id, "path": path})
t0 = time.time()
try:
rows = read_rows(path)
logger.debug("loaded rows=%d", len(rows))
cleaned = clean_numeric(rows)
logger.info("cleaned rows=%d", len(cleaned))
except FileNotFoundError:
logger.error("file not found", exc_info=True)
raise
except Exception as e:
logger.error("clean step failed", exc_info=True)
raise
duration = time.time() - t0
logger.info(
"step=complete",
extra={"batch_id": batch_id, "duration_s": duration, "rows": len(cleaned)}
)
return cleaned
This pattern gives you: start/complete markers, row counts, error detail with tracebacks, and timing — all with consistent context.
Related Cheat Sheets¶
- python-testing — logging helps when tests need diagnostics
- pytest — configure log capture and assertion helpers
- debugging — logs and the debugger complement each other
- ml-preprocessing — logging transform steps helps diagnose pipeline issues
- model-monitoring — production observability builds on logging habits
Official Documentation¶
Last Updated¶
2026-09-06 — Initial version.