Skip to content

Python logging — Structured Application Logs

The logging module is the standard way to record what your program is doing. Unlike print, it has severity levels, can route messages to files or the network, includes timestamps and source info, and can be turned up or down without touching your code.

quickstart.py
import logging
 
logging.basicConfig(level=logging.INFO)
logging.info("App started")
logging.warning("Low disk space")
logging.error("Connection failed")
# INFO:root:App started
# WARNING:root:Low disk space
# ERROR:root:Connection failed
printlogging
One firehose to stdout.Severity levels you can filter.
No timestamps or context.Timestamps, module, line number, etc.
Edit code to silence it.Change one config line.
Goes one place.Files, console, syslog, email — many handlers.

Each message has a level. The logger only emits messages at or above its configured level.

LevelValueUse for
DEBUG10Detailed diagnostic info.
INFO20Normal events (“server started”).
WARNING30Something unexpected, but still working (the default).
ERROR40A failure in some operation.
CRITICAL50A serious failure; the program may stop.
levels.py
import logging
 
logging.basicConfig(level=logging.DEBUG)
logging.debug("variable x = 42")     # shown only because level is DEBUG
logging.info("processing record")
logging.warning("retrying")
logging.error("gave up")
print(logging.getLevelName(20))      # INFO

The default level is WARNING, so debug and info messages are hidden until you lower the level with basicConfig(level=...).

basicConfig configures the root logger in one call. Set it once, early, before logging anything.

basic_config.py
import logging
 
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
    datefmt="%H:%M:%S",
)
logging.info("ready")
# 14:30:01 [INFO] root: ready
 
# Write to a file instead of the console
logging.basicConfig(filename="app.log", level=logging.INFO)
PlaceholderInserts
%(asctime)sHuman-readable timestamp.
%(levelname)sThe level name (INFO, ERROR, …).
%(name)sThe logger’s name.
%(message)sThe log message.
%(filename)s / %(lineno)dSource file / line number.

Instead of the root logger, create a logger per module with getLogger(__name__). This tags every message with where it came from and lets you tune modules independently.

named_loggers.py
import logging
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
logger.info("module-specific message")
# INFO:__main__:module-specific message
sketch A disabled log line is not free p5.js
The logger is at WARNING, so every debug call below is discarded. It still costs something, and the usual advice -- use percent-style so formatting is lazy -- only removes half the cost. The argument is evaluated before the call either way: in the lazy form the expensive function still runs, only the string building is deferred. Measured per call: 180.3 ns lazy, 271.0 ns with an f-string, and 84.7 ns when guarded by isEnabledFor.

For real apps, attach handlers (where logs go) and formatters (how they look). You can have several at once — e.g. INFO to console, ERROR to a file.

handlers.py
import logging
 
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
 
# Console handler at INFO
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
 
# File handler at DEBUG
file = logging.FileHandler("debug.log")
file.setLevel(logging.DEBUG)
 
logger.addHandler(console)
logger.addHandler(file)
 
logger.debug("only in the file")
logger.info("console + file")

Inside an except block, logger.exception(...) logs the message plus the full traceback.

exceptions.py
import logging
 
logging.basicConfig(level=logging.ERROR)
try:
    1 / 0
except ZeroDivisionError:
    logging.exception("Math went wrong")
# ERROR:root:Math went wrong
# Traceback (most recent call last): ...
  • Default level hides INFO/DEBUG — call basicConfig(level=logging.DEBUG) to see them.
  • basicConfig only works once — the first call wins; later calls are ignored unless force=True.
  • Use %s lazy formatting: logger.info("user %s", name), not f-strings, so the string is only built if the message is actually emitted.
  • Don’t log secrets — passwords, tokens, and personal data should never hit the logs.
  • logging replaces print with leveled, timestamped, routable messages.
  • Levels: DEBUG < INFO < WARNING < ERROR < CRITICAL; default is WARNING.
  • Configure quickly with basicConfig(level=..., format=...).
  • Use getLogger(__name__) per module, and attach handlers/formatters for fine control.
  • Log tracebacks with logger.exception inside except blocks.

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading