print() is fine for debugging, but production applications need logging — configurable, filterable, and writable to files, databases, or monitoring services.

Basic Logging

  import logging

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

logger.debug("Detailed debug info")
logger.info("Application started")
logger.warning("Disk space low")
logger.error("Failed to connect to database")
logger.critical("System shutting down")
  

Output:

  INFO:__main__:Application started
WARNING:__main__:Disk space low
  

Log Levels

Level Value Use Case
DEBUG 10 Detailed diagnostic info
INFO 20 General operational messages
WARNING 30 Something unexpected but not fatal
ERROR 40 A serious problem
CRITICAL 50 Program may be unable to continue

Set level to control verbosity — INFO in production, DEBUG in development.

Formatting Output

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

Output:

  2024-06-15 10:30:00 [INFO] myapp: Server started on port 8000
  

Handlers — Multiple Outputs

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

# Console handler
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))

# File handler
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
    "%(asctime)s [%(levelname)s] %(name)s:%(lineno)d — %(message)s"
))

logger.addHandler(console)
logger.addHandler(file_handler)
  

Module-Level Loggers

Each module gets its own logger:

  # services/api.py
import logging
logger = logging.getLogger(__name__)

def fetch_data(url):
    logger.info(f"Fetching {url}")
    try:
        ...
    except ConnectionError:
        logger.error(f"Connection failed for {url}", exc_info=True)
        raise
  

exc_info=True includes the full traceback.

Logging Exceptions

  try:
    result = 10 / 0
except ZeroDivisionError:
    logger.exception("Calculation failed")
    # Same as: logger.error("...", exc_info=True)
  

Configuration from File

  import logging.config

LOGGING_CONFIG = {
    "version": 1,
    "formatters": {
        "standard": {
            "format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "standard",
            "level": "INFO",
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "filename": "app.log",
            "maxBytes": 10_000_000,
            "backupCount": 5,
            "formatter": "standard",
            "level": "DEBUG",
        },
    },
    "root": {
        "handlers": ["console", "file"],
        "level": "DEBUG",
    },
}

logging.config.dictConfig(LOGGING_CONFIG)
  

Structured Logging (JSON)

For log aggregation tools (ELK, Datadog, CloudWatch):

  import json
import logging

class JSONFormatter(logging.Formatter):
    def format(self, record):
        return json.dumps({
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "logger": record.name,
            "message": record.getMessage(),
        })

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

Or use python-json-logger package.

Best Practices

  1. Use logging, not print() in application code
  2. One logger per modulelogger = logging.getLogger(__name__)
  3. Log at the right level — don’t log everything as ERROR
  4. Include context — user ID, request ID, operation name
  5. Never log secrets — passwords, tokens, API keys
  6. Use rotating file handlers to prevent disk fill
  7. Configure via environment — log level from env var
  import os
level = os.getenv("LOG_LEVEL", "INFO")
logging.basicConfig(level=getattr(logging, level))
  

Proper logging is essential for debugging production issues and monitoring application health.