← Back to Course Index
Lesson 15 of 20

Logging

~15 min · logging module, levels, handlers, formatters, best practices

Ref
Primary Source
Official HOWTO — Logging

The definitive guide. Start with the "Basic Logging Tutorial" section. Also essential: Logging Cookbook.

1 — Why Not Just print()?

JavaScript
// Console levels exist but are cosmetic
console.log("info message");
console.warn("warning");
console.error("error");
console.debug("debug");

// Production: use a library like pino, winston
import pino from "pino";
const log = pino({ level: "info" });
log.info({ userId: 42 }, "User logged in");
log.error({ err }, "Something broke");
Python — logging module (stdlib)
import logging

# Levels: DEBUG < INFO < WARNING < ERROR < CRITICAL
logging.debug("Low-level detail")
logging.info("Normal operation")
logging.warning("Something unexpected")
logging.error("Something failed")
logging.critical("Application cannot continue")

# Each level has a numeric value:
# DEBUG=10, INFO=20, WARNING=30, ERROR=40, CRITICAL=50

Unlike print(), the logging module gives you: level filtering, timestamps, caller info, multiple output destinations (file + console), and the ability to silence library noise — all configurable without changing code.

2 — Basic Setup

import logging

# Minimal setup — call once, at the top of your main script
logging.basicConfig(
    level=logging.DEBUG,          # show all levels ≥ DEBUG
    format="%(asctime)s %(levelname)-8s %(name)s — %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

# Now use it
logging.info("Server started on port 8080")
logging.warning("Config file not found, using defaults")
logging.error("Database connection failed")

# Output:
# 2026-06-23 22:30:01 INFO     root — Server started on port 8080
# 2026-06-23 22:30:01 WARNING  root — Config file not found, using defaults
# 2026-06-23 22:30:01 ERROR    root — Database connection failed

3 — Named Loggers (the right way)

Always use logging.getLogger(__name__) in your modules — not the root logger. This makes it easy to silence or configure specific parts of your app.

# In every module file:
import logging
logger = logging.getLogger(__name__)
# __name__ = "myapp.database" if file is myapp/database.py

def connect(url: str):
    logger.info("Connecting to %s", url)   # use %s, not f-strings (lazy formatting!)
    try:
        conn = db.connect(url)
        logger.debug("Connection established in %.2fms", elapsed)
        return conn
    except Exception as e:
        logger.exception("Failed to connect to %s", url)  # logs ERROR + full traceback!
        raise
💡 Use % formatting, not f-strings

Write logger.info("User %s logged in", user_id) not logger.info(f"User {user_id} logged in"). With %, the string is only formatted if the message will actually be logged — saving CPU on DEBUG messages in production.

4 — Handlers & Formatters

Handlers send log records to destinations. You can attach multiple handlers — e.g. console + file simultaneously.

import logging
import sys

def setup_logging(level: str = "INFO") -> None:
    """Call once from your main() function."""

    root = logging.getLogger()
    root.setLevel(logging.DEBUG)  # capture everything; handlers filter

    # Console handler — INFO and above
    console = logging.StreamHandler(sys.stdout)
    console.setLevel(getattr(logging, level.upper()))
    console.setFormatter(logging.Formatter(
        "%(asctime)s %(levelname)-8s %(name)s — %(message)s",
        datefmt="%H:%M:%S"
    ))

    # File handler — DEBUG and above (full detail)
    file_h = logging.FileHandler("app.log", encoding="utf-8")
    file_h.setLevel(logging.DEBUG)
    file_h.setFormatter(logging.Formatter(
        "%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)d — %(message)s"
    ))

    root.addHandler(console)
    root.addHandler(file_h)

# main.py
if __name__ == "__main__":
    setup_logging("INFO")
    main()

5 — Silencing Noisy Libraries

# Third-party libraries (requests, httpx, etc.) use logging too.
# You often want to silence their verbose DEBUG output.

import logging

# Silence specific library loggers
logging.getLogger("urllib3").setLevel(logging.WARNING)
logging.getLogger("httpx").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)

# Or silence everything below WARNING globally, except your own code
logging.getLogger().setLevel(logging.WARNING)
logging.getLogger("myapp").setLevel(logging.DEBUG)  # your app: full detail

6 — Structured Logging (production)

For production systems, use structlog or python-json-logger for JSON output that log aggregators (Datadog, CloudWatch, etc.) can parse.

pip install structlog
import structlog

log = structlog.get_logger()

log.info("user.login", user_id=42, ip="192.168.1.1")
# {"event": "user.login", "user_id": 42, "ip": "192.168.1.1", "timestamp": "..."}

log.error("payment.failed", order_id="ORD-99", amount=99.99, error="Card declined")
# Machine-readable JSON — perfect for log aggregation

7 — Quick Reference: Common Format Specifiers

SpecifierMeaning
%(asctime)sTimestamp
%(levelname)sDEBUG / INFO / WARNING etc.
%(name)sLogger name (__name__)
%(message)sThe log message
%(filename)sSource file
%(lineno)dLine number
%(funcName)sFunction name

🧠 Quiz

1. What does logger.exception("msg") log compared to logger.error("msg")?

2. Why use logger.info("User %s", uid) instead of logger.info(f"User {uid}")?

3. You want DEBUG in your app's log file but only WARNING on the console. How?

0/3

Questions answered correctly.