Skip to content

← Config, Logs and Untrusted Input step 11 of 16

Easy Primitives

Log from a Library the Right Way

Your team's shop package has an inventory module. A web shop uses it and wants only warnings in its logs. A nightly report job uses it too and wants every detail while someone debugs it. If the library prints, or sets up logging itself, it chooses for both of them. Instead, a library sends its messages to a named logger and lets each application decide what to show.

import logging

logger = logging.getLogger("shop.inventory")

logger.debug("reserving %d of %s", 3, "apples")  # hidden unless the app asks for DEBUG
logger.warning("%s is low: %d left", "pears", 4)

Logger names form a tree by their dots. shop.inventory is a child of shop, and shop is a child of the root logger. Each logging call creates a record. A record logged on shop.inventory travels up the tree, and every handler on the way receives it. A handler is the object that decides where records go: a file, the terminal, or a list in a test. A logger with no level of its own uses the level of its nearest ancestor that has one.

What to write

Fill in reserve(stock, item, quantity), library code for the shop.inventory module.

  • At module level, create the logger once with logging.getLogger("shop.inventory").
  • On every call, log at DEBUG: "reserving %d of %s", with quantity and item as arguments.
  • Take quantity away from stock[item]. If fewer than LOW_STOCK are left, log at WARNING: "%s is low: %d left", with item and the number left.
  • Return the number left.
  • Pass the values as arguments after the message, not inside an f-string. The logger then builds the text only if a handler needs it.
  • Do not add handlers, set levels, or call logging.basicConfig() or logging.warning(). Those choices belong to the application.

In a real module you would write logging.getLogger(__name__), and Python fills in the module's dotted name. The grader runs your code without a module name, so __name__ there is "builtins". Write the name out.

Edge cases

  • Exactly LOW_STOCK left is not low. Warn only below it.
  • No test asks for more than is in stock, so you do not need to handle running out. Taking all of an item leaves 0, which is low.

What the driver reports

solve plays the application. It adds a handler to the shop logger and sets that logger's level. The handler keeps every record in a list. Then solve makes the calls and removes everything it added. It returns:

  • lines: each record that got through, as LEVEL logger: message.
  • values_as_args: True if every record kept its values as arguments instead of a ready-made string.
  • library_handlers and library_level: the number of handlers and the level set on the shop.inventory logger itself. A library should leave these as 0 and "NOTSET".
  • root_handlers_added: the number of handlers that appeared on the root logger. logging.basicConfig() adds one, and so does a call such as logging.warning() when nothing is set up. A library should add none.
  • stock: what is left of each item.

Going further

A library can add logging.NullHandler() to its top-level logger, here shop. Records still travel up to the application's handlers, and the library's warnings no longer print on their own when the application has set up no logging at all.