Desk Clarity

A field guide to everyday digital problems.

Developer tools / Practical guide

Python Logging Prints Twice: Inspect Handler Placement and Propagation

One logging call produces duplicate output because the same record reaches handlers on a logger and its ancestors.

By ON Lab2 min readSources checked November 10, 2026 (KST)

Check the named logger and its ancestor handlers before removing logging calls. With propagation enabled, a record can be emitted by a local handler and another handler higher in the hierarchy.

Check the failure with a small fixture

One logging call produces duplicate output because the same record reaches handlers on a logger and its ancestors.

import io, logging
output = io.StringIO()
root = logging.getLogger()
root.handlers.clear()
root.setLevel(logging.INFO)
def handler(prefix):
    h = logging.StreamHandler(output)
    h.setFormatter(logging.Formatter(prefix + ":%(message)s"))
    return h
root.addHandler(handler("ROOT"))
log = logging.getLogger("fixture.worker")
log.handlers.clear()
log.setLevel(logging.INFO)
log.addHandler(handler("LOCAL"))
log.propagate = True
log.info("one event")
assert output.getvalue() == "LOCAL:one event\nROOT:one event\n"
output.seek(0); output.truncate(0)
log.handlers.clear()
log.info("one event")
assert output.getvalue() == "ROOT:one event\n"
logging.shutdown()

What the result means

Run this isolated fixture in a disposable process because it intentionally changes logging configuration. Different prefixes show two emissions of the same call, rather than two calls to the application logger. Clearing the local handlers leaves ancestor propagation as the single output route.

Choose the correction for your caller

For a centrally configured application, attach handlers at the chosen high-level logger and allow children to propagate. If a component deliberately owns an independent handler, disabling propagation on that component can be appropriate instead. Inspect repeated initialization too: adding a new handler every time a function runs can duplicate output even with propagation disabled. hasHandlers checks ancestors as well, so it is not equivalent to testing whether this logger has local handlers.

Limits and the next check

Do not clear framework handlers in a running service merely to try this example. Inspect logger names, local handler counts and propagation without logging secrets. Multiprocess logging, notebook output capture and separate handlers writing the same file require their own ownership review.

Related: Why a later basicConfig call can be ignored.

The embedded Python example was executed as an isolated local script on November 10, 2026 (KST), using Python 3.11.6. It demonstrates the stated boundary; it is not a test of your application or a promise of compatibility with every Python release.

Sources & further reading