stdlib logging cheatsheet. For structured logging, see Cheatsheet 08 .

Basic

import logging

logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)

log.debug("...")
log.info("...")
log.warning("...")
log.error("...")
log.critical("...")
log.exception("...")  # logs + stack trace from current except block

Named logger per module

# In each module
log = logging.getLogger(__name__)

Logger names form a hierarchy: myapp.services.users is a child of myapp.services.

basicConfig (simple)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(name)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
    stream=sys.stdout,
)

dictConfig (production)

LOG_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "json": {
            "()": "pythonjsonlogger.jsonlogger.JsonFormatter",
            "format": "%(asctime)s %(name)s %(levelname)s %(message)s",
        },
        "console": {
            "format": "%(asctime)s %(levelname)s %(name)s: %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "console" if not PROD else "json",
            "stream": "ext://sys.stdout",
        },
    },
    "loggers": {
        "": {  # root
            "level": "INFO",
            "handlers": ["console"],
        },
        "uvicorn": {"level": "WARNING"},
        "sqlalchemy.engine": {"level": "WARNING"},
    },
}

import logging.config
logging.config.dictConfig(LOG_CONFIG)

Handlers

# File
fh = logging.FileHandler("app.log")
log.addHandler(fh)

# Rotating
from logging.handlers import RotatingFileHandler
rh = RotatingFileHandler("app.log", maxBytes=10*1024*1024, backupCount=5)

# Timed rotating
from logging.handlers import TimedRotatingFileHandler
trh = TimedRotatingFileHandler("app.log", when="midnight", backupCount=7)

# Syslog
from logging.handlers import SysLogHandler
sh = SysLogHandler(address=("syslog.local", 514))

# SMTP (alert via email)
from logging.handlers import SMTPHandler
smtp = SMTPHandler(
    mailhost="smtp.example.com",
    fromaddr="[email protected]",
    toaddrs=["[email protected]"],
    subject="App alert",
)
smtp.setLevel(logging.CRITICAL)

For prod: stream to stdout; let your log shipper handle files / rotation / SMTP.

Formatters

fmt = logging.Formatter(
    "%(asctime)s %(name)s %(levelname)s %(message)s",
    datefmt="%Y-%m-%dT%H:%M:%S",
)
handler.setFormatter(fmt)

Available fields:

%(asctime)s
%(name)s
%(levelname)s
%(message)s
%(filename)s
%(lineno)d
%(funcName)s
%(thread)d
%(threadName)s
%(process)d

Custom fields (extra)

log.info("user_login", extra={"user_id": 42, "ip": "..."})

# In formatter:
"%(asctime)s %(message)s user_id=%(user_id)s ip=%(ip)s"

Doesn’t error if extras are missing? Use a custom Filter to add defaults.

Filters

class RequestIDFilter(logging.Filter):
    def filter(self, record):
        record.request_id = current_request_id.get() or "-"
        return True

logger.addFilter(RequestIDFilter())

Levels

DEBUG     (10)
INFO      (20)
WARNING   (30)  (default for root)
ERROR     (40)
CRITICAL  (50)

Set per-logger:

logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)

Log to stdout (Docker / K8s)

import sys
handler = logging.StreamHandler(sys.stdout)

Don’t log to files in containers; let the orchestrator collect stdout.

JSON logging (without structlog)

uv add python-json-logger
from pythonjsonlogger import jsonlogger

handler = logging.StreamHandler()
formatter = jsonlogger.JsonFormatter(
    "%(asctime)s %(name)s %(levelname)s %(message)s"
)
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)

Suppress noisy libraries

logging.getLogger("urllib3.connectionpool").setLevel(logging.WARNING)
logging.getLogger("asyncio").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)

Capture warnings

logging.captureWarnings(True)
# Now Python warnings (deprecation, etc.) go through logging

Common mistakes

  • print for logging — no level, no structure.
  • f-string in log message: log.info(f"user {user.id}") — formats even if not logged. Use log.info("user %s", user.id).
  • Logging at DEBUG in production — high volume.
  • PII in log messages.

Read this next

If you want my dictConfig + JSON + per-module setup, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .