Skip to content

Python Logging

Before adding more behavior to the application, we need a way to see what's happening inside it. print() is fine for quick scripts, but production applications need structured, configurable logging. Python's standard library logging module covers that.

The simplest possible setup is one line at app startup:

Python
import logging

logging.basicConfig(level=logging.INFO)

That tells Python to send INFO-and-above log messages to the console.

This one-line setup surfaces issues:

  • how to name loggers per-module
  • what the levels mean
  • and how to format log values without creating formatted strings that might not ever get logged

The Logger Object

Instead of calling a global log function, create a logger object for each module. The convention is to name it using the special __name__ variable, which Python automatically sets to the module's path (for example, release_tracker.crud).

Python
import logging

logger = logging.getLogger(__name__)

This naming convention creates a hierarchy. If the root release_tracker logger is configured to only show errors, that configuration flows down to release_tracker.crud and release_tracker.main.

Log Levels

Loggers use severity levels to filter messages. The standard levels, in increasing order of severity:

  1. DEBUG: Detailed diagnostic information.
  2. INFO: General operational events.
  3. WARNING: Something unexpected happened, but the application is still running.
  4. ERROR: A serious problem occurred, and a specific operation failed.
  5. CRITICAL: A fatal error that may cause the application to crash.

Use the method that matches the message's severity:

Python
logger.debug("Parsing request payload")
logger.info("Project created successfully")
logger.warning("Rate limit approaching")
logger.error("Database connection failed")

The configured level acts as a floor: messages at or above it are emitted, anything below is dropped. Production typically runs at INFO; flipping to DEBUG is normal in development.

Lazy Formatting

When logging variables, you might be tempted to use f-strings (logger.debug(f"Payload: {payload}")). The right pattern is "lazy formatting":

Python
# Do this:
logger.debug("Parsing payload: %s", payload)

# Not this:
logger.debug(f"Parsing payload: {payload}")

Info

Lazy formatting (%s) skips the string interpolation entirely if the message won't be emitted at the current log level.

Linters like Ruff and Pylint enforce this with the G004 / logging-fstring-interpolation rule. Many production codebases turn it on.

Production teams often ship logs to a structured logging tool like Datadog or Splunk, which indexes each value (status_code=500, path=/projects) as its own searchable field. That's much more useful than searching for substrings inside a long log line. With %s, the values stay as separate arguments that a library like structlog can forward as fields.