C.W.K.
Stream
Lesson 05 of 06 · published

logging — Doing It Right

~18 min · logging, logger, handler, formatter

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete

Why logging instead of print

print is for prototypes. logging is for code that lives. Logging gives you levels (DEBUG/INFO/WARNING/ERROR/CRITICAL), handlers (where to write), formatters (how it looks), and the ability to turn it on/off without touching the code. Once you've used it for a week, going back to print feels primitive.

The five levels

DEBUG — verbose tracing for development. INFO — events that mark normal operation. WARNING — something unusual but not failing. ERROR — a failure that didn't crash the program. CRITICAL — about to die. The level you set on a logger filters everything below it: setting INFO drops DEBUG.

logger.info(), not logging.info(), in libraries

The standard library pattern: at the top of each module, logger = logging.getLogger(__name__). Use logger.info(...), never logging.info(...). The named logger lets the application configure verbosity per-module — useful when you want noisy debug from one module and quiet from another. __name__ gives you the import path automatically.

basicConfig — for scripts

For a script that just needs logs to stderr or a file, logging.basicConfig(level=INFO, format='...') is the one-liner. For larger applications, configure handlers and formatters explicitly — that's the next code block.

Principle: Libraries call logger.info("..."). Applications configure logging once at the top — what level, what format, where to write. Don't call basicConfig from a library; let the application decide.

Code

Quick start — basicConfig for a script·python
import logging

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
)

logger = logging.getLogger(__name__)

logger.debug("detailed info — only shown at DEBUG level")
logger.info("normal operation event")
logger.warning("something looks off")
logger.error("a failure occurred")
logger.critical("emergency!")

# Output (level=INFO drops DEBUG):
# 2026-05-02 ... [INFO] __main__: normal operation event
# 2026-05-02 ... [WARNING] __main__: something looks off
# 2026-05-02 ... [ERROR] __main__: a failure occurred
# 2026-05-02 ... [CRITICAL] __main__: emergency!
Library pattern — getLogger(__name__)·python
# my_library.py
import logging

logger = logging.getLogger(__name__)        # named after the module

def do_work(item):
    logger.debug("starting work on %r", item)
    try:
        result = item.upper()
    except AttributeError:
        logger.error("item %r is not a string", item)
        raise
    logger.info("processed %r", item)
    return result

# In the application:
# import logging
# logging.basicConfig(level=logging.INFO)
# logging.getLogger('my_library').setLevel(logging.DEBUG)   # noisy this module
Logging exceptions — exc_info and exception()·python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

def divide(a, b):
    try:
        return a / b
    except Exception:
        # logger.exception logs at ERROR level WITH a traceback — equivalent to
        # logger.error("...", exc_info=True)
        logger.exception("divide(%s, %s) failed", a, b)
        return None

result = divide(10, 0)
# ERROR ... divide(10, 0) failed
# Traceback (most recent call last):
#   ...
# ZeroDivisionError: division by zero
Format strings — lazy formatting·python
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

user = "alice"
count = 42

# DON'T — formats even at level DEBUG (wasted work if filtered out)
logger.debug(f"user {user} has count {count}")

# DO — % formatting, only formats if the level is enabled
logger.debug("user %s has count %d", user, count)

# Same idea for info/warning/error
logger.info("connected to %s on port %d", "localhost", 8000)
Multiple handlers — file + stderr·python
import logging

logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)

# Handler 1 — stderr at INFO
stream = logging.StreamHandler()
stream.setLevel(logging.INFO)
stream.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
logger.addHandler(stream)

# Handler 2 — file at DEBUG (everything)
file_h = logging.FileHandler("/tmp/app.log", mode="a")
file_h.setLevel(logging.DEBUG)
file_h.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
logger.addHandler(file_h)

logger.debug("detail — only in the file")
logger.info("important — in both stderr and file")

External links

Exercise

Build a small module quester.py that defines def play(level: str) which logs at all 5 levels. Use logging.getLogger(__name__). In a separate script, configure logging with basicConfig at INFO level, then explicitly set the quester logger's level to DEBUG so its DEBUG messages also appear. Confirm both observations: (a) other modules' DEBUG is silenced, (b) quester's DEBUG is shown.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.