Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
How-tos

Stop Printing Debug Messages: Logging Best Practices in Python

Learn actionable Python logging best practices to replace print() statements, use proper log levels, handle exceptions with tracebacks, avoid sensitive data leaks, and keep logs manageable in production.

July 2026 9 min read 1 views 0 hearts

We've all been there. Your script fails at 2 AM, and the only thing you left yourself is a few print() statements that got buried in terminal noise. Or worse—you print() something important, but it scrolls off the screen before you can read it.

Logging isn't just for "real" applications. It's for survival. And Python's logging module, while powerful, has a few sharp edges that beginners—and even experienced developers—often cut themselves on.

Let's walk through the most actionable logging practices I've picked up from maintaining a PythonSkillset backend that processes hundreds of thousands of events daily. These aren't academic guidelines. They're lessons learned from real debugging sessions.

Use the Right Log Level (And Actually Mean It)

Python gives you five standard levels: DEBUG, INFO, WARNING, ERROR, CRITICAL. Sounds simple, but I see misuse constantly.

Here's the rule of thumb I follow at PythonSkillset:

  • DEBUG: Things only I care about while developing. Variable values, function entry points, loop counters. Never leave DEBUG on in production.
  • INFO: Confirmation that things are working normally. "User 12345 logged in", "Report generation completed". Useful for dashboards.
  • WARNING: Something unexpected happened, but the system recovered. "Rate limit approaching for API key X". Don't ignore warnings—they often predict errors.
  • ERROR: Something failed, but the application keeps running. "Failed to send email notification for order 9876, retrying..."
  • CRITICAL: The application cannot continue. "Database connection pool exhausted. Shutting down."

Be honest with your levels. Don't log everything as ERROR just to get attention. That creates noise, and noise kills signal.

Stop Using print() for Logging

I know it's tempting. It's fast, it's easy, and it shows up in the terminal. But print() has no levels, no format control, no file rotation, no timestamp by default.

When you use print(), you're throwing away:

  • Structured data (machine-readable logs)
  • Timestamps (when did this happen?)
  • Log levels (was this critical or just informative?)
  • Output redirection (can't easily separate debug from errors)

The logging module takes three lines to set up correctly. It's not a luxury—it's a necessity.

The One Config Pattern You Need

Here's a no-fuss setup I use in almost every Python project at PythonSkillset:

import logging
import sys

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

Why this works:

  • StreamHandler to stdout: See important messages in real-time in your terminal.
  • FileHandler to app.log: Persistent record you can grep, analyze, or feed into log aggregators.
  • Clear format with timestamp: You know exactly when something happened, at what level, and in which module.
  • Date format: No timezone confusion. Add %Z if you need timezone.

Set level=logging.DEBUG during development, then switch to logging.WARNING in production. Or use environment variables:

import os
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
logging.basicConfig(level=getattr(logging, log_level, logging.INFO))

Use Logger Objects, Not Root Logger

The classic beginner mistake:

import logging
logging.info("User signed up")

This uses the root logger. It works, but it's messy. Once you have multiple modules, you lose traceability.

Instead, create a logger per module:

logger = logging.getLogger(__name__)
logger.info("User signed up")

Now every log message includes the module name. When you're hunting a bug in a system with 50 components, knowing exactly which file produced the log is gold.

Log Exceptions Properly

Don't do this:

try:
    result = risky_operation()
except Exception as e:
    logger.error(f"Something went wrong: {e}")

Why? Because str(e) doesn't include the traceback. You lose the line number and call stack.

Do this instead:

try:
    result = risky_operation()
except Exception:
    logger.exception("Something went wrong")

logger.exception() automatically includes the full traceback at ERROR level. You don't even need to pass the exception object.

If you're using logging.error() and want the traceback, pass exc_info=True:

logger.error("Something went wrong", exc_info=True)

Avoid Logging Sensitive Data

This seems obvious, but I've seen production logs with passwords, API keys, and even credit card numbers. At PythonSkillset, we had to scrub a week's worth of logs once because someone accidentally logged a full user data dump.

General rule: Never log raw request bodies, password fields, auth tokens, or personal identifiers.

If you need to log something for debugging, create a sanitized version:

def sanitize_for_logs(data):
    sensitive_keys = {"password", "token", "secret", "ssn"}
    return {k: "***" if k.lower() in sensitive_keys else v for k, v in data.items()}

Log Context, Not Just Messages

A log that says "Failed to connect" is useless. A log that says "Failed to connect to database 'analytics' on host 10.0.1.5:5432 after 3 retries" saves you hours.

Include relevant context:

logger.warning("Slow query detected", extra={
    "query_id": query_id,
    "duration_ms": duration_ms,
    "user_id": user_id
})

If you're using structured logging (JSON format), this becomes even more powerful. You can feed logs into Elasticsearch, Splunk, or Datadog and query by any field.

Don't Log in Hot Paths

Logging is I/O. I/O is slow. If you log every single line inside a loop that runs 10,000 times per second, your application will crawl.

Strategies:

  • Use DEBUG level for high-frequency logs. Suppress in production.
  • Add rate limiting to log handlers for noisy events.
  • Sample logs: "Every 100th event, log the details."
import random
if random.random() < 0.01:  # 1% sampling
    logger.debug("Event processed", extra={"event_id": event_id})

Rotate Log Files

Logs grow. Your disk doesn't. A single app can generate gigabytes per day.

Use RotatingFileHandler or TimedRotatingFileHandler:

from logging.handlers import RotatingFileHandler

handler = RotatingFileHandler(
    "app.log", 
    maxBytes=10_000_000,  # 10 MB
    backupCount=5
)

This keeps your last 50MB of logs. Old logs get deleted automatically. You'll never wake up to a full disk.

The Final Takeaway

Good logging is not about writing more logs. It's about writing the right logs at the right level with the right context.

Start with that simple configuration. Use logger = logging.getLogger(__name__) in every module. Log exceptions with logger.exception(). And please—stop printing debug messages to stdout in production.

Your future self, debugging at 2 AM, will thank you.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.