skip to content

Why does a Python library attach logging.NullHandler to its logger and configure nothing else?

level: middleimportance: should knowfreq 33%

answer

  1. Policy belongs to the process
  2. Emit, do not configure
  3. The one handler that writes nothing
  4. It suppresses the stderr fallback
  5. Records still propagate to the application

basics

~20 s

A library emits records and lets the importing application choose destinations. logging.NullHandler discards records, so an application that configured no logging sees nothing instead of a surprise stderr fallback, and one that did still receives everything by propagation.

solid answer

~40 s

Handlers are policy and belong to whoever runs the process. A library that calls `basicConfig`, adds a `logging.StreamHandler`, or sets a level on the root logger hijacks that decision for every application that imports it. The convention is instead: name your loggers with `getLogger(__name__)`, log freely, and attach a single `logging.NullHandler` to your package's top-level logger. Because a record with no handler anywhere in its chain is written to standard error by the module's `logging.lastResort` handler, an unconfigured application would otherwise see warnings from a library it never configured; the null handler counts as a handler found, so that fallback stays silent. Nothing else changes: when the application does install handlers on the root, the library's records propagate up and are written normally.

code

python · 8 lines
python
import logging

bare = logging.getLogger("flagsdk.bare")
bare.warning("no handler anywhere: logging.lastResort prints this to stderr")

quiet = logging.getLogger("flagsdk.quiet")
quiet.addHandler(logging.NullHandler())
quiet.warning("a NullHandler counts as handled, so nothing is printed")

go deeper

for a junior

Recall the split: libraries emit records, applications choose destinations. Knowing that a null handler exists and discards records is enough at this level.

for a middle

Explain why the fallback to standard error exists, how a handler that writes nothing still counts as found and suppresses it, and why propagation to the application's handlers is unaffected.

for a senior

Demonstrate the diagnosis both ways — unexpected stderr warnings from a dependency, and a dependency that logs nothing — and treat the library's top-level logger name as part of its public surface.

for a principal

Own the rule across internal packages: no shared library configures logging, entry points do; publish logger names as contract, and review any handler added outside the application's single configuration point.

### Who owns the destination Python's logging module splits responsibility deliberately. Naming a logger and emitting records is the job of the code that has something to say; deciding where those records go, in what format, at what verbosity, is the job of the process that is running. A library lives in someone else's process. If it calls `logging.basicConfig`, adds its own `logging.StreamHandler`, or sets a level on the root logger at import time, it silently takes over an application-wide decision — and does so at whatever point in the import order it happens to be reached. So the rule for library code is short: call `logging.getLogger(__name__)`, log, and configure nothing. Records propagate up the dotted hierarchy to the application's handlers all by themselves, so the library needs no destination of its own to be useful. ### The one exception, and why it exists There is exactly one handler a library should attach, and it writes nothing. When a record finishes the walk up its ancestors without meeting a single handler, the logging module falls back to `logging.lastResort`, a handler that writes warnings and above to standard error with no formatting. That fallback is a kindness for scripts, but from a library's point of view it means an application that never configured logging at all can suddenly see raw warning text from a dependency it did not know was talking. Attaching `logging.NullHandler()` to the library's top-level logger — the package name, so every module beneath it is covered — prevents that. The null handler's emit does nothing, but it is a handler, and the fallback fires only when no handler was found. Note the ordering inside the walk: a handler is counted as found *before* its level is checked, so even a handler that rejects everything suppresses the fallback. That is precisely the property the null handler relies on. It costs nothing else. When the application does configure a handler on the root logger, the library's records still propagate past the null handler and reach it, formatted like everything else. The null handler neither swallows records nor stops propagation; those are different mechanisms. ### The lost-message symptoms this explains Two complaints come out of the same model. "The library prints warnings I never asked for" is the missing null handler: nothing was configured, so the fallback wrote to standard error. "The library logs nothing at all" is usually the mirror — the application configured a handler somewhere that this logger's chain does not reach, or set the level so nothing is created, or the library's own logger has propagation switched off by some well-meaning helper. Checking, in order, whether the record is created, whether it propagates, and whether any handler on the chain accepts it turns both complaints into a two-minute diagnosis. A third variant is worth naming: a library that attaches a real handler rather than a null one produces duplicated output in every application that has also configured the root, and the application author cannot easily remove it without reaching into the library's logger. The null handler is the version of that gesture that is safe to ship. ### What a library should offer instead Give the application handles rather than decisions. Use dotted names that make prefixes meaningful, so an application can raise verbosity for one part of the library without the rest. Document the top-level logger name — it is part of your public surface, as much as a function signature. Keep the emitting code free of any assumption about formatting, since the formatter belongs to whatever handler the application installed. If your library is also an entry point — a command-line tool shipped from the same package — put the configuration in the command-line code path, not at import time. Being imported and being run are different situations, and only the second one has the right to decide where logs go. ### The application side of the same contract The mirror obligation belongs to the application: configure once, early, in the entry point, and configure the root logger rather than reaching into each dependency's logger by name. Because every library's records propagate upward, one handler on the root is enough to capture all of them, and per-library tuning is then a matter of raising or lowering a dotted prefix rather than installing more destinations. An application that instead attaches a handler to each dependency it cares about is signing up for the duplication and ordering problems the null-handler convention exists to avoid.

  • Does attaching logging.NullHandler stop the library's records reaching the application's handlers?
    No. The null handler discards the copy given to it and nothing more; the walk continues up the hierarchy, so any handler the application installed on an ancestor or the root still receives the record and formats it normally. Suppressing that upward walk is a different mechanism entirely — switching the logger's propagation off — and a library should not do it.
  • What does a Python application see from a library that never attaches a null handler?
    If the application configured no logging at all, warnings and above from that library are written to standard error unformatted by `logging.lastResort`, which looks like stray output from nowhere. If the application did configure handlers, nothing looks unusual — which is why the missing null handler often survives until someone embeds the library in a quiet script or a service that logs elsewhere.
  • Where should a library attach its null handler in the logger hierarchy?
    On the package's top-level logger — `logging.getLogger("mypkg")`, typically in the package's `__init__` module — because every module below inherits the effect through the walk up the ancestors. One attachment covers the whole library, and it keeps the library's public logger name explicit, which is the name applications will target when they want to raise or lower its verbosity.

saying these in an interview costs you the question

  • Calls basicConfig from library code at import time
  • Thinks a null handler swallows records the application wants
  • Adds a stream handler in a library for convenience
  • Sets a level on the root logger from a library
  • Confuses adding a null handler with switching propagation off

context