skip to content

In Python's logging module, what jobs do Handler, Formatter and Filter each do?

level: middleimportance: should knowfreq 48%

answer

  1. Three jobs, never merged
  2. Destination, rendering, predicate
  3. The formatter belongs to a handler
  4. Filters attach in two different places
  5. Propagation skips ancestor filters

basics

~20 s

A Handler is a destination and writes the record there. A Formatter belongs to one handler and turns the record into text. A Filter is a predicate that drops or edits records, attached to a handler or to a logger.

solid answer

~50 s

A `logging.Logger` decides only whether a record exists; everything after that belongs to handlers. A `logging.Handler` subclass owns one destination — a stream, a file, a socket — and each handler carries its own level and its own `logging.Formatter`, set with `setFormatter`, that renders the record into text. That pairing is why the same record can appear as one terse line on the console and a fully-detailed line in a file: two handlers, two formatters. A `logging.Filter` is a finer sieve than a level: attached to a handler it drops records on the way into that destination, attached to a logger it drops records logged directly through it. The important asymmetry is that a filter on a logger is *not* applied to records that propagate up from its children, while a handler's filter sees everything that reaches it.

code

python · 19 lines
python
import logging
import sys


class OnlyFlags(logging.Filter):
    def filter(self, record):
        return record.name.startswith("flags")


handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("%(name)s | %(levelname)s | %(message)s"))
handler.addFilter(OnlyFlags())

root = logging.getLogger()
root.setLevel(logging.INFO)
root.addHandler(handler)

logging.getLogger("flags.store").info("kept")
logging.getLogger("web.api").info("dropped by the filter")

go deeper

for a junior

Recall the split in one sentence each: handler writes somewhere, formatter makes the text, filter says yes or no. Knowing that a formatter is attached to a handler is the piece most often missed.

for a middle

Explain the pipeline in order — logger level, record, logger filters, walk the ancestors, handler level, handler filters, formatter, emit — and why two handlers give two differently-shaped copies of one event.

for a senior

Demonstrate the placement judgement: redaction belongs on the handler that guards a destination, because a filter on a parent logger never sees records propagated from its children.

for a principal

Own where these pieces are declared at all: one configuration surface per service, handlers as the only place destinations appear, and filters as the enforced boundary for redaction rather than ad-hoc code at call sites.

### One record, three collaborators Emitting a log line in Python runs a short pipeline, and each participant has exactly one job. The logger creates a `logging.LogRecord` and decides whether the record is worth creating at all. A handler owns a destination and writes there. A formatter, owned by a handler, turns the record into a string. A filter is an arbitrary predicate that can drop a record anywhere it is attached. Keeping those roles apart is what makes the module configurable instead of hard-coded. ### Handler: the destination `logging.Handler` is the abstract base; the concrete ones you meet first are `logging.StreamHandler` (a stream such as standard error) and `logging.FileHandler`. A handler carries its own level, its own list of filters and exactly one formatter. Its `emit` method does the writing; `handle` wraps that with the filter check and locking. Add one with `addHandler` — on the root logger for application-wide output, or on a dotted prefix when one subsystem needs its own destination. Because the handler's level is independent of the logger's, a single logger can feed a console handler at warning level and a file handler at debug level from the same records. The logger's level gates whether the record is made; the handler's level gates whether that destination is interested. When people say "my debug lines never appear", the record was usually created and then rejected by every handler. ### Formatter: the rendering `logging.Formatter` converts a record into text. You give it a format string — `"%(asctime)s %(name)s %(levelname)s %(message)s"` in the default percent style, with brace and template styles available via its `style` argument — plus an optional date format used by `formatTime`. Attach it with the handler's `setFormatter`. A handler with no formatter prints just the message. Two properties matter in practice. Formatting is *deferred*: the record holds the message and its arguments, and the text is produced only when a handler that will actually emit asks for it. And a formatter is per-handler, not per-logger, which is precisely how the same event becomes a compact console line and a rich file line at the same time. One formatter object can be shared by several handlers. ### Filter: the fine sieve `logging.Filter` — or any callable with the same shape — answers yes or no for a record, with logic a level cannot express: a name prefix, a substring, a rate limit, a field the application attached. Attach it with `addFilter` on a handler to guard that destination, or on a logger to guard what that logger itself emits. The asymmetry between those two placements is the detail interviewers probe. A logger runs its filters when *it* is the logger the call was made on. When the record then propagates upward, only handler levels and handler filters are consulted — ancestor loggers' filters and levels are skipped entirely. So a filter placed on a parent logger to scrub records from a whole subtree silently does nothing for records logged by the children. Put it on the handler instead, where every record that reaches the destination must pass. Since Python 3.12 a filter may also *return a record* rather than a boolean, which replaces the record for the rest of the pipeline; returning any falsy value still drops it. That makes a filter the sanctioned place to enrich or redact a record in flight, not only to reject it. ### Putting the pipeline in order For a call on logger `L`: `L` checks its level, builds the record, runs `L`'s own filters, then walks `L` and its ancestors. For every handler found, the handler's level is checked, then the handler's filters, then `emit` renders through the formatter and writes. Knowing that order makes diagnosis mechanical. Nothing at all appears: no handler was found, or every one of them rejected the level. Output appears but is unformatted: a handler with no formatter. Output appears twice with different shapes: two handlers, each with its own formatter, both reached during the walk. A drop you expected did not happen: the filter is on a logger the record only passed *through*, not on the handler. ### Where to attach what The practical division follows from the roles. Destinations are an application concern, so handlers belong to the process's single configuration point. Presentation follows the destination, so formatters are declared with the handler they serve and never anywhere else. Policy that must hold for a destination no matter who logged — redaction, sampling, a correlation requirement — belongs to a filter on that handler, because it is the only place guaranteed to see every record heading there. Policy that is genuinely local to one component, such as suppressing a chatty code path, belongs to a filter on that component's own logger, where it is visible next to the code it governs.

  • Can one logging.Handler hold more than one formatter?
    No — a handler has exactly one formatter, set with `setFormatter`, and it renders every record that handler emits. When you need two shapes of output you need two handlers, each with its own formatter, both reachable during the walk up the hierarchy. The formatter object itself is stateless enough to be shared across handlers if you do want identical output in several destinations.
  • When would you reach for a Filter rather than a level?
    When the decision is not about severity. A level is a single ordered threshold; a filter is arbitrary code over the record, so it can key off the logger name, a substring, a caller-supplied field, or a rate limit. It is also the right place for redaction, since a filter on a handler sees every record heading for that destination, wherever in the hierarchy it was logged.
  • Why is a filter placed on a parent logger ineffective for records from its children?
    Propagation is a handler walk, not a logger walk. The originating logger applies its own level and filters, then the record is offered only to the handlers found on that logger and its ancestors — ancestor loggers' filters and levels are never consulted. To catch a whole subtree, attach the filter to the handler the subtree's records reach, or attach it to each logger that actually makes the calls.

saying these in an interview costs you the question

  • Thinks a formatter is set on the logger
  • Believes a parent logger's filter screens its children's records
  • Cannot separate the handler's level from the logger's
  • Assumes one handler can format two ways
  • Treats filters as only a second kind of level

context