The logging Module and Log Levels
16 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)When a program fails on a server, nobody is watching the screen. A log is a record the program writes about what it did. The logging module adds levels, a clear format and a choice of where the lines go, none of which print can do.
Why not just print
Logging is recording what a program does as it runs, as lines with a level, a time or a name. Settings control which lines show and where they go, without changing the code that writes them.
A print line cannot be turned off, labelled or sent to a file without editing the code. A log call states how serious the message is, and a setting decides which ones to show.
import logging
logging.warning("Disk is 85% full")Example 1 · The default level hides INFO
import loggingimport sys
logging.basicConfig(stream=sys.stdout, format="%(levelname)s:%(message)s")logging.info("starting")logging.warning("disk is 85% full")logging.error("could not save order")WARNING:disk is 85% full ERROR:could not save order
What this code does
- No level was set, so the root logger uses
WARNING. - The
infoline is dropped. The other two are shown.
With no setup, only warnings and above appear, on the error stream, as WARNING:root:Disk is 85% full.
The five levels
Log level is a number that says how serious a message is: DEBUG 10, INFO 20, WARNING 30, ERROR 40, CRITICAL 50. A logger shows only messages at or above its own level.
Each level has a number. DEBUG (10) is detail for developers, INFO (20) is normal events, WARNING (30) is something odd that still works, ERROR (40) is a failed task, CRITICAL (50) is a failure of the whole program.
import logging
print(logging.DEBUG, logging.INFO, logging.WARNING)print(logging.getLevelName(40))Example 2 · Level DEBUG shows everything
import loggingimport sys
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout, format="%(levelname)-8s %(message)s")logging.debug("cache key = a12")logging.info("order saved")logging.warning("retrying")logging.error("payment failed")logging.critical("database is down")DEBUG cache key = a12 INFO order saved WARNING retrying ERROR payment failed CRITICAL database is down
What this code does
%(levelname)-8spads the level name to 8 characters so messages line up.- All five lines show, from the lowest level to the highest.
A logger shows a message only when its level is at or above the logger's own level. The default level is WARNING.
basicConfig sets the format
logging.basicConfig sets up the root logger once. Choose the level, the format and the stream. The format uses %(levelname)s, %(message)s, %(name)s and %(asctime)s.
import loggingimport sys
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="%(levelname)s:%(message)s")logging.info("Server started")logging.debug("hidden")Example 3 · Format and named loggers
import loggingimport sys
logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="[%(levelname)s] %(name)s - %(message)s")orders = logging.getLogger("shop.orders")orders.info("order created")logging.getLogger("shop.stock").warning("only 3 left")[INFO] shop.orders - order created [WARNING] shop.stock - only 3 left
What this code does
%(name)sshows which logger wrote the line, so you know which part of the program spoke.- Both loggers pass their lines to the root logger we configured.
Only INFO:Server started appears. A second basicConfig call does nothing, because the root logger already has a handler.
Pass values as arguments
Write logger.info("Order %s total %d", order_id, total), not an f-string. Logging fills in the values only if the message will be shown, so hidden messages cost almost nothing. Inside except, logger.exception(...) also records the traceback.
import logging
logger = logging.getLogger("shop")order_id, total = "A12", 500logger.info("Order %s total %d", order_id, total)Example 4 · Arguments are filled in only when needed
import loggingimport sys
class Report: def __str__(self): print("building text") return "big report"
logging.basicConfig(level=logging.WARNING, stream=sys.stdout, format="%(message)s")logging.debug("data: %s", Report())logging.warning("data: %s", Report())building text data: big report
What this code does
- The
debugcall is below the level, so__str__never runs and "building text" is not printed for it. - The
warningcall is shown, so the value is turned into text first.
The %s and %d fill-ins are the old % style, which is what logging uses.
Next step
Check your understanding
11 quick questions