OSPython.032: Custom Log Formatting with logging.Formatter — Timestamps, Levels, Logger Names, and Message Layout

Realistic programming workstation showing Python logging.Formatter code and formatted log output

Elementary overview: Python’s logging.Formatter controls the final layout of a log line. The logger creates the event, a handler sends it to a destination, and the formatter turns the record into readable text.

This lesson continues the logging sequence from OSPython.028: Python Logging Basics, OSPython.029: Logging to Files with FileHandler, OSPython.030: Rotating Log Files, and OSPython.031: Time-Based Log Rotation. Here, the focus is simple: make each log line easy to scan, sort, and troubleshoot.

What You Should Learn

  • What a Formatter does—and what it does not do.
  • How to show timestamps, severity, logger names, and messages.
  • How to attach a formatter to a handler.
  • How datefmt and formatter styles work.
  • How to keep console and file logs readable without overloading each line.
Diagram showing a Python LogRecord passing through logging.Formatter to become readable log output
A Formatter controls presentation: it turns LogRecord fields such as time, level, logger name, and message into the final text emitted by a handler.

The Mental Model: Logger → Handler → Formatter → Output

A logger creates a LogRecord. A handler decides where that record goes, such as the terminal or a file. The formatter decides how that accepted record looks when the handler emits it.

Formatter means presentation. It does not decide the event’s severity, and it does not choose the destination.

Python loggers, handlers, and formatters explained.

Build a Useful First Format

import logging

formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)

That format asks Python to include four pieces of context: the event time, severity level, logger name, and final message. A line might look like this:

Example output: 2026-10-08 19:22:41,104 | ERROR | app.database | Connection failed.

The Fields Worth Learning First

FieldMeaningWhy it helps
%(asctime)sEvent timeShows when the event occurred
%(levelname)sDEBUG, INFO, WARNING, ERROR, or CRITICALShows severity
%(name)sLogger nameShows which subsystem produced the record
%(message)sFinal event messageShows what happened
%(lineno)dSource line numberUseful when tracing where a record originated

Python exposes many more LogRecord fields, including filename, funcName, process, and threadName. Add them only when they improve diagnosis. A log line overloaded with fields can become harder to read than a smaller, consistent format.

Attach the Formatter to a Handler

import logging

logger = logging.getLogger("api")
logger.setLevel(logging.INFO)

handler = logging.StreamHandler()

formatter = logging.Formatter(
    "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info("Server started")
logger.warning("Response time is high")

The key line is handler.setFormatter(formatter). The formatter belongs to the handler that emits the record. That means two handlers attached to the same logger can use different layouts.

Control Timestamps with datefmt

datefmt changes how %(asctime)s is displayed. Without a custom date format, Python’s default formatter uses a date-and-time representation with milliseconds. Python uses local time by default for formatter timestamps unless you deliberately change the formatter’s time converter.

formatter = logging.Formatter(
    "%(asctime)s | %(levelname)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

For operations work, consistency matters more than decoration. Pick a timestamp format that is easy to compare across terminal output, log files, services, and incident timelines.

Choose a Formatter Style

logging.Formatter supports three template styles: percent (%), brace ({), and dollar ($). Percent style is the default and remains the most common in Python logging examples.

# Percent style — default
logging.Formatter(
    "%(levelname)s | %(name)s | %(message)s"
)

# Brace style
logging.Formatter(
    "{levelname} | {name} | {message}",
    style="{",
)

# Dollar style
logging.Formatter(
    "$levelname | $name | $message",
    style="$",
)

The style option changes the formatter template, not the way arguments passed to calls such as logger.info() are merged into the event message.

Software Testing Mentor demonstrates advanced Python logging with loggers, handlers, and formatters.

For Small Scripts: basicConfig

import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logging.info("Application started")

For a small script, logging.basicConfig() can define the format directly. Once an application has several destinations or different severity requirements, explicit logger, handler, and formatter objects are easier to reason about.

A directly relevant Python logging example showing formatters used to add timestamps, log levels, and other context.

One Logger, Two Different Formats

A console usually benefits from a compact format, while a file may need more troubleshooting context. Because formatters attach to handlers, both can receive the same record and present it differently.

console_handler = logging.StreamHandler()
file_handler = logging.FileHandler("app.log")

console_handler.setFormatter(
    logging.Formatter("%(levelname)s | %(message)s")
)

file_handler.setFormatter(
    logging.Formatter(
        "%(asctime)s | %(levelname)s | %(name)s | %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
)

This pattern connects directly to FileHandler, RotatingFileHandler, and TimedRotatingFileHandler. Rotation controls how files are managed; the formatter controls what each line looks like.

Common Formatter Mistakes

  • Creating a formatter but never attaching it: use handler.setFormatter().
  • Expecting Formatter to filter records: filtering and formatting are separate jobs.
  • Mixing template styles: brace syntax requires style="{"; dollar syntax requires style="$".
  • Leaving out message: a custom format can accidentally hide the event text.
  • Adding too much context: every extra field increases visual noise.
  • Adding handlers repeatedly: duplicate handlers can create duplicate log lines.

Troubleshooting Checklist

  1. Confirm the handler is actually attached to the logger.
  2. Confirm the formatter is attached to the handler.
  3. Check logger and handler levels separately.
  4. Verify that every placeholder exists on the LogRecord.
  5. Check that the chosen style matches the template syntax.
  6. If lines are duplicated, inspect logger hierarchy and repeated handler setup.
  7. Test the real emitted output instead of assuming the configuration is correct.

Lab

  1. Create a logger named inventory.
  2. Add a StreamHandler.
  3. Create a formatter with asctime, levelname, name, and message.
  4. Add datefmt="%Y-%m-%d %H:%M:%S".
  5. Attach the formatter with setFormatter().
  6. Emit INFO, WARNING, and ERROR messages.
  7. Add filename and lineno, then decide whether the extra detail improves readability.
  8. Create a second handler with a shorter format and compare the two outputs.

Knowledge Check + Answers

  1. What does logging.Formatter control? The final rendered layout of a log record.
  2. Where is a formatter normally attached? To a handler with handler.setFormatter(formatter).
  3. What does %(asctime)s show? A formatted timestamp for the record.
  4. What does %(levelname)s show? The textual severity level.
  5. What does %(name)s show? The name of the logger that produced the record.
  6. Does formatter style change logger.info() message interpolation? No. It changes the formatter template.
  7. Can two handlers use different formatters? Yes.
  8. Why might logs appear twice? One common cause is duplicate handlers or logger propagation, which is covered more deeply in the next lesson.

Primary Technical References

Elementary Review

Logger creates the record. Handler chooses the destination. Formatter chooses the presentation. For a strong beginner format, start with time, severity, logger name, and message. Keep it consistent before adding more fields.

Next Python Lesson

OSPython.033: Logger Hierarchy and Propagation will explain parent and child logger names, propagation, handler inheritance, and why duplicate log lines sometimes appear.

Editor’s Note

The featured artwork is a unique 1200×630 realistic programming scene created specifically for OSPython.032 and is not reused inside the lesson body. The separate body diagram explains the LogRecord → Formatter → output flow. Neon green is limited to the small bitcoinversus.tech tag at bottom-left.

BitcoinVersus.tech is not a financial advisor. Content is provided for informational purposes.

Leave a comment