Course topics

By WebNest Studio

Python Tutorial

Logging in Python

print() is fine while learning, but real applications need logs that have timestamps and severity levels, can be switched on and off per module, go to files or monitoring systems, and never crash the program. Python's built-in logging module provides all of this and is used by virtually every framework, including FastAPI, Django and Flask.

This lesson covers log levels, basic configuration, named loggers, formatting, handlers for files and rotation, logging exceptions, structured JSON logs, and configuration with dictConfig.

Log Levels

Five standard levels in increasing severity: DEBUG (detailed diagnostics), INFO (normal events: "order placed"), WARNING (unexpected but handled: "retrying payment"), ERROR (an operation failed) and CRITICAL (the application cannot continue). A logger only outputs messages at or above its configured level; the default is WARNING.

Loggers, Handlers and Formatters

Create a logger per module with logging.getLogger(__name__); names form a hierarchy (shop.payments is a child of shop), so you can tune levels per package. Handlers decide where records go (StreamHandler for the console, FileHandler, RotatingFileHandler, TimedRotatingFileHandler), and formatters decide how they look (%(asctime)s %(levelname)s %(name)s %(message)s).

Good Logging Practice

Configure logging once, at the application's entry point, never in library modules. Pass values as arguments (log.info("user %s logged in", user)) so formatting only happens when the message is emitted. Use log.exception() inside except blocks to include the traceback. Never log passwords, tokens or full card numbers. In production, JSON (structured) logs are easier to search in tools like Elasticsearch, Loki or CloudWatch.

Examples

Levels and basic configuration

Python
import logging
import sys

logging.basicConfig(level=logging.INFO, stream=sys.stdout,
                    format="%(levelname)-8s %(name)s: %(message)s",
                    force=True)   # replace handlers left over from an earlier run
log = logging.getLogger("shop")

log.debug("cart contents: %s", ["pen"])      # below INFO: not shown
log.info("order %s placed by %s", "WN-101", "asha")
log.warning("payment slow, retrying (attempt %d)", 2)
log.error("payment failed for order %s", "WN-102")
log.critical("database unreachable")
Output
INFO     shop: order WN-101 placed by asha
WARNING  shop: payment slow, retrying (attempt 2)
ERROR    shop: payment failed for order WN-102
CRITICAL shop: database unreachable

Named loggers, per-module levels and logging exceptions

Python
import logging
import sys

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter("[%(levelname)s] %(name)s - %(message)s"))
root = logging.getLogger()
root.addHandler(handler)
root.setLevel(logging.WARNING)

logging.getLogger("shop.payments").setLevel(logging.DEBUG)   # verbose for one package

logging.getLogger("shop.catalog").info("catalog loaded")      # suppressed (WARNING level)
pay_log = logging.getLogger("shop.payments")
pay_log.debug("calling gateway")

try:
    1 / 0
except ZeroDivisionError:
    pay_log.exception("fee calculation failed")
Output
[DEBUG] shop.payments - calling gateway
[ERROR] shop.payments - fee calculation failed
Traceback (most recent call last):
  File "main.py", line 16, in <module>
    1 / 0
    ~~^~~
ZeroDivisionError: division by zero

Rotating log files and dictConfig

Python
import logging
import logging.config

logging.config.dictConfig({
    "version": 1,
    "formatters": {
        "detailed": {"format": "%(asctime)s %(levelname)s %(name)s: %(message)s"},
    },
    "handlers": {
        "console": {"class": "logging.StreamHandler", "formatter": "detailed", "level": "INFO"},
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": "app.log",
            "maxBytes": 1_000_000,       # rotate at ~1 MB
            "backupCount": 5,            # keep app.log.1 ... app.log.5
            "formatter": "detailed",
            "level": "DEBUG",
        },
    },
    "root": {"handlers": ["console", "file"], "level": "DEBUG"},
})

log = logging.getLogger("webnest")
log.info("application started")
log.debug("this detail goes only to app.log")
Output
2026-09-27 10:15:02,118 INFO webnest: application started
(app.log contains both the INFO and the DEBUG line)

Structured JSON logs with a custom formatter

Python
import json
import logging
import sys

class JsonFormatter(logging.Formatter):
    def format(self, record):
        entry = {"level": record.levelname, "logger": record.name, "message": record.getMessage()}
        entry.update(getattr(record, "context", {}))
        return json.dumps(entry)

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(JsonFormatter())
log = logging.getLogger("api")
log.handlers.clear()          # avoid duplicate lines if this code runs twice
log.addHandler(handler)
log.setLevel(logging.INFO)
log.propagate = False

log.info("order placed", extra={"context": {"order_id": "WN-101", "amount": 2999}})
Output
{"level": "INFO", "logger": "api", "message": "order placed", "order_id": "WN-101", "amount": 2999}

Common Mistakes

  • Using print() for diagnostics in applications instead of logging.
  • Calling logging.basicConfig inside library modules.
  • Formatting messages with f-strings in hot paths; pass arguments instead.
  • Logging secrets or personal data.
  • Catching exceptions and logging only str(e), losing the traceback — use log.exception().

Key Points to Remember

  • Levels: DEBUG < INFO < WARNING < ERROR < CRITICAL; default level is WARNING.
  • Use logging.getLogger(__name__) per module; configure once at startup.
  • Handlers send records to console/files (with rotation); formatters shape output.
  • log.exception() records tracebacks; JSON formatters give structured logs.
  • dictConfig centralises logging configuration.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.

Try Levels and basic configuration in Webnest CodelabTry Structured JSON logs with a custom formatter in Webnest Codelab