Something crashed at 3am. I checked the logs. This is what I found:

Error connecting
Retrying...
Done
10
User not found

No timestamps. No context. "User not found"—which user? When? What endpoint? I spent 2 hours just figuring out which part of the code produced these messages.

That was the day I stopped using print() for anything beyond quick debugging.

What's Wrong with print()

For quick scripts, print() is fine. But in production:

Good logs answer: when, where, what, and how bad.

Logging Basics

Python's logging module is built-in. Here's the minimum you need:

import logging

logging.basicConfig(level=logging.INFO)

logging.debug("Detailed stuff, usually hidden")
logging.info("Normal operation, good to know")
logging.warning("Something unexpected, but not broken")
logging.error("Something broke")
logging.critical("Everything is on fire")

With level=logging.INFO, debug messages are hidden. Bump it to DEBUG when troubleshooting, set to WARNING in production to reduce noise.

The killer feature: you can leave debug logs in your code forever. They're silent until you need them.

Adding Timestamps and Context

basicConfig works for scripts. For real apps, set up proper formatting:

import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logging.info("Server started")

Output:

2025-03-22 10:30:00,123 - root - INFO - Server started

Now you know exactly when it happened.

Logging to Files

Console output disappears when the process dies. Write to files too:

import logging

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

# Console handler - only warnings and above
console = logging.StreamHandler()
console.setLevel(logging.WARNING)

# File handler - everything
file_handler = logging.FileHandler('app.log')
file_handler.setLevel(logging.DEBUG)

# Format
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
console.setFormatter(formatter)
file_handler.setFormatter(formatter)

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

Now your console stays clean (just warnings and errors) while the file captures everything for later analysis.

Rotating Logs

Log files grow forever. I've seen servers crash because a log file consumed all disk space.

from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    'app.log',
    maxBytes=5*1024*1024,  # 5MB
    backupCount=3           # Keep 3 old files
)
logger.addHandler(handler)

When app.log hits 5MB, it becomes app.log.1, and a fresh app.log starts. Old files get rotated out. Total disk usage stays bounded.

Structured Logging (JSON)

Text logs are great for humans reading a terminal. They're terrible for searching across thousands of servers.

Modern log aggregation tools (ELK, Datadog, CloudWatch) work better with JSON:

import json
import logging

class JsonFormatter(logging.Formatter):
    def format(self, record):
        log_data = {
            "timestamp": self.formatTime(record),
            "level": record.levelname,
            "message": record.getMessage(),
            "module": record.module,
        }
        if record.exc_info:
            log_data["exception"] = self.formatException(record.exc_info)
        return json.dumps(log_data)

handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
logger.addHandler(handler)

logger.info("User logged in", extra={"user_id": 123})

Output:

{"timestamp": "2025-03-22 10:30:00", "level": "INFO", "message": "User logged in", "module": "auth"}

Now you can query: "show me all ERROR logs from the auth module in the last hour."

Logging Exceptions

When catching exceptions, use logger.exception()—it automatically includes the stack trace:

try:
    risky_operation()
except Exception:
    # ❌ Loses the stack trace
    logger.error("Operation failed")
    
    # ✅ Includes full traceback
    logger.exception("Operation failed")

Quick Tips

Switching from print() to logging feels like extra work. Until the first time you're debugging a production issue at 2am and your logs actually tell you what happened. Then it feels like a superpower.

← Back to Python Articles

Back to Home