Loggers, Handlers and Formatters
18 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)basicConfig is one shortcut over three parts. A logger receives the call, a handler sends the line somewhere, and a formatter decides how the line looks. Knowing the parts lets you log to the screen and a file at once.
Loggers have names
Logger is the object your code calls to record a message. It has a name, a level and handlers. Loggers form a tree by their dotted names, with the root logger at the top.
logging.getLogger("shop") returns the logger called shop. Asking again for the same name gives the same object. A dot in the name makes a family: shop.orders is a child of shop.
import logging
shop = logging.getLogger("shop")print(shop is logging.getLogger("shop"))print(logging.getLogger("shop.orders").parent.name)Example 1 · Named loggers
import loggingimport sys
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(name)s: %(message)s")shop = logging.getLogger("shop")orders = logging.getLogger("shop.orders")print(orders.parent is shop)print(logging.getLogger("shop") is shop)orders.info("order created")shop.info("shop is open")True True shop.orders: order created shop: shop is open
What this code does
ordersis a child ofshop, because of the dot in its name.- Each line shows the name of the logger that wrote it.
In a module, write logger = logging.getLogger(__name__). The name then follows the module path.
Handlers choose the destination
Handler is the part of logging that sends a record to a destination, such as the screen, a file or a network service. It has its own level and its own formatter.
A handler sends each record to one place: StreamHandler to a stream such as the screen, FileHandler to a file. Every handler has its own level, so the screen can show warnings while the file keeps everything.
import loggingimport sys
logger = logging.getLogger("shop")handler = logging.StreamHandler(sys.stdout)handler.setLevel(logging.WARNING)logger.addHandler(handler)Example 2 · Two handlers, two levels
import ioimport loggingimport sys
logger = logging.getLogger("shop")logger.setLevel(logging.DEBUG)
screen = logging.StreamHandler(sys.stdout)screen.setLevel(logging.WARNING)screen.setFormatter(logging.Formatter("%(message)s"))
buffer = io.StringIO()keep = logging.StreamHandler(buffer)keep.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
logger.addHandler(screen)logger.addHandler(keep)logger.debug("cache miss")logger.error("payment failed")print("--- kept ---")print(buffer.getvalue().strip())payment failed --- kept --- DEBUG:cache miss ERROR:payment failed
What this code does
- The screen handler shows only the warning and above, with a short layout.
- The second handler keeps every record with the level name. A
StringIOstands in for a file, so we can print what it holds.
A logger can hold several handlers, and each record goes to all of them that accept its level.
Formatters choose the layout
Formatter is the object that turns a log record into a line of text, using a format string such as "%(levelname)s | %(message)s". A handler uses one formatter.
A Formatter is a format string. Set it on a handler with setFormatter. Two handlers can show the same record in two layouts: short on screen, full in a file.
import logging
fmt = logging.Formatter("%(levelname)s | %(name)s | %(message)s")handler = logging.StreamHandler()handler.setFormatter(fmt)Add %(asctime)s for the time. Time changes every run, so the examples in this course leave it out.
Records travel up the tree
After a logger's own handlers, the record passes to its parent's handlers, and so on up to the root. This is propagation. Put handlers on one high logger, and every child uses them. Adding the same handler at two levels prints each line twice.
import logging
child = logging.getLogger("shop.orders")child.propagate = FalseExample 3 · A child uses the parent's handler
import loggingimport sys
parent = logging.getLogger("shop")parent.setLevel(logging.INFO)handler = logging.StreamHandler(sys.stdout)handler.setFormatter(logging.Formatter("%(name)s: %(message)s"))parent.addHandler(handler)
logging.getLogger("shop.orders").warning("low stock")logging.getLogger("shop.stock").info("counted")shop.orders: low stock shop.stock: counted
What this code does
- Neither child has a handler. Their records travel up to the handler on
shop. - The level of the child is inherited from
shop, so both lines show.
Example 4 · The duplicate-line bug and propagate
import loggingimport sys
def make(name, with_handler): logger = logging.getLogger(name) logger.setLevel(logging.INFO) if with_handler: logger.addHandler(logging.StreamHandler(sys.stdout)) return logger
make("app", True)child = make("app.db", True)child.info("connected")print("---")child.propagate = Falsechild.info("connected again")connected connected --- connected again
What this code does
- The child has its own handler and so does its parent, so the first line prints twice.
- After
propagate = Falsethe record stops at the child, and it prints once.
Setting propagate = False stops the record at that logger. A reusable library should add only a NullHandler and let the application choose the handlers.
Next step
Check your understanding
11 quick questions