Advertisement
πŸ“‹ Real-World

Python Logging – logging Module & Best Practices

Print statements break in production. Python's built-in logging module provides levels, formatting, file output, and rotation β€” everything needed to debug and monitor real applications.

⏱️ 20 min read🎯 Real-WorldπŸ“… Updated 2026

Basic Setup

Python
import logging

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s | %(levelname)-8s | %(name)s | %(message)s",
    datefmt="%Y-%m-%d %H:%M:%S",
)

logger = logging.getLogger(__name__)
logger.debug("Diagnostic info")
logger.info("Normal event")
logger.warning("Unexpected but non-fatal")
logger.error("Something failed")
logger.critical("System cannot continue")

File & Rotating Handlers

Python
from logging.handlers import RotatingFileHandler

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

fmt = logging.Formatter("%(asctime)s | %(levelname)-8s | %(name)s:%(lineno)d | %(message)s")

console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(fmt)

# Rotate at 5 MB, keep 3 backups
file_h = RotatingFileHandler("app.log", maxBytes=5_000_000, backupCount=3)
file_h.setLevel(logging.DEBUG)
file_h.setFormatter(fmt)

logger.addHandler(console)
logger.addHandler(file_h)

Structured JSON Logging

Python
import json

class JSONFormatter(logging.Formatter):
    def format(self, record):
        entry = {
            "time": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        }
        if record.exc_info:
            entry["exception"] = self.formatException(record.exc_info)
        return json.dumps(entry)

handler = logging.StreamHandler()
handler.setFormatter(JSONFormatter())
logging.getLogger("api").addHandler(handler)

Logging Exceptions

Python
try:
    result = 10 / 0
except ZeroDivisionError:
    logger.exception("Division failed")  # Auto-includes traceback

logger.info("User login", extra={"user_id": 42, "ip": "127.0.0.1"})
Tip: Use logging.getLogger(__name__) in every module β€” creates a hierarchy so you can control log levels per module.

Logging Beats print() β€” Here's Why

print() is fine for a quick check, but real applications use the logging module. The difference: logging has severity levels you can filter, can route output to files/services, and can be turned down in production without editing code.

import logging
logging.basicConfig(level=logging.INFO,
                    format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger(__name__)

log.debug("detailed trace")     # hidden unless level=DEBUG
log.info("server started")
log.warning("disk 80%% full")
log.error("payment failed", exc_info=True)   # includes the traceback
LevelUse for
DEBUGdeveloper diagnostics
INFOnormal events
WARNINGsomething unexpected but handled
ERROR / CRITICALfailures

Key practices: set the level once at startup β€” flip it from INFO to DEBUG to get more detail with zero code changes, the whole point over print. Use getLogger(__name__) per module so you can see where messages came from. Pass exc_info=True (or use log.exception() inside an except) to capture the stack trace. Security: never log passwords, tokens, or full card numbers β€” logs get shipped, stored, and read by many people.

πŸ‹οΈ Practical Exercise

Set up proper logging:

  1. Configure logging with logging.basicConfig and log messages at different levels.
  2. Add a file handler so logs are written to a file as well as the console.
  3. Use a RotatingFileHandler to cap log file size.
  4. Log an exception with logging.exception() inside an except block.

πŸ”₯ Challenge Exercise

Add structured logging to a small script: create a named logger, log at DEBUG/INFO/WARNING/ERROR levels appropriately, write to both console and a rotating file, and format messages with timestamp, level, and module. In an error path, capture the full traceback with logging.exception. Bonus: emit logs as JSON lines so they can be ingested by a log aggregator.

πŸ“‹ Summary

  • The logging module provides leveled, configurable, and reusable diagnostics β€” far better than print.
  • Levels in order: DEBUG, INFO, WARNING, ERROR, CRITICAL.
  • A logger emits records, handlers route them (console, file), and formatters shape their text.
  • logging.exception() logs an error message together with the current traceback.
  • Rotating handlers cap log size by rolling over to new files.
  • Structured/JSON logs are easy for tools to parse, search, and aggregate.

Interview Questions on Logging

  • Why use the logging module instead of print?
  • What are the standard logging levels and when do you use each?
  • What is the difference between a logger, a handler, and a formatter?
  • What does logging.exception() add over logging.error()?
  • What is a rotating file handler and why is it useful?
  • How do you configure logging for a larger application?
  • What is structured (e.g. JSON) logging and why does it help?

FAQ

Why is logging better than print? +

Logging adds severity levels, timestamps, and source info, can be routed to files or external systems, and can be turned up or down without editing code. Prints have to be removed manually and offer none of this control.

What is the difference between a logger, handler, and formatter? +

A logger is the entry point you call (logger.info(...)). A handler decides where records go (console, file, network). A formatter defines how each record looks (timestamp, level, message). One logger can have several handlers.

When should I use each log level? +

DEBUG for detailed developer diagnostics, INFO for normal events, WARNING for unexpected-but-handled situations, ERROR for failures in an operation, and CRITICAL for severe problems threatening the whole program.

What does logging.exception() do? +

Called inside an except block, it logs your message at ERROR level and automatically appends the full traceback, so you capture both the context and the stack of the failure in one call.