3. Logging#

Something broke. Add a print statement! Fixed. Take it out! We’ve all been here. A steady stream of adding and (hopefully) removing print statements. But there is a better way, if you are willing to pay the (rather ugly) cost of setting it up: Logging. Here’s what it looks like (yes, it looks like it was designed in the 80’s, even though Python only dates back to ‘91):

import logging
# Global setting
logging.basicConfig(level="INFO")

This is a global setting that affects all loggers, including the ones in the libraries you are using (hopefully). You can also change in individual logger.

Next we get a logger, these are usually given names that match the package they are in (globally unique). If there is no logger to match, one is created.

log = logging.getLogger("unique")
log.warning("Very important")
WARNING:unique:Very important
log.info("Logging this here")
INFO:unique:Logging this here
log.debug("Logging this here")
log.setLevel("DEBUG")

log.debug("Try again")
DEBUG:unique:Try again

You can see that messages at or above the current level get printed out. You can set fancier handlers, too, which can add timestamps and such.

This is very powerful for adding printouts that only show up if you ask for info or debug printouts (the normal setting is “WARN”). Sadely the design is very old, with classic % style formatting baked in (use f-strings in the logging messages, though; that works well), global logger pool, and such. See Rich for a much more beautiful setting.

The hardest part is generally setting up the infrastructure for controlling the logger, usually; it’s best if you have a flag or environment variable that can control this, and you have to decide or allow a choice on whether you want all loggers or just yours to change level. And you have might want to log to a file, rotate logs, etc; everything doable but not all that pretty.

3.1. Test logging#

We’ll cover testing later, but you can have pytest add your logs whenever tests fail! This can save a lot of time debugging failures.

This is the configuration line to do that:

[tool.pytest.ini_options]
log_cli_level = "info"

3.2. More logging#

If you need more from your logging, check out structlog! Also rich can print beautiful logs. (And yes, you can combine structlog and rich!)

This is how you would use Rich:

from rich.logging import RichHandler

FORMAT = "%(message)s"
logging.basicConfig(
    level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)

log = logging.getLogger("rich")
log.info("Hello, World!")

(Must be set on first call to basicConfig)

This should be set for an application, not a library - libraries should stick to defaults and let applications override things like this.