The Python Logging Setup I Actually Use in 2026 (And Should Be Default)

The Python Logging Setup I Actually Use in 2026 (And Should Be Default)

The Problem With Default Python Logging

Look, I’ve shipped enough Python projects to know this pattern: you start with print() statements, graduate to basic logging.info(), and eventually end up with a mess of different logging configs across your services. Then one day you’re debugging in production and realize half your logs are missing while httpx is dumping every HTTP header into your terminal.

Logger Hierarchy : Python’s logging system uses a tree structure where loggers inherit configuration from parent loggers, allowing you to control verbosity at different levels of your application.

The default logging setup in Python is… fine. But it’s not great. You get plain text output, no color coding, and every third-party library thinks its DEBUG logs are fascinating. I’ve wasted too many hours scrolling past httpx connection details to find my actual application logs.

Why Rich Changes Everything

I stumbled into using Rich’s logging handler about two years ago, and honestly, I can’t go back. The difference is night and day:

  • Automatic syntax highlighting for variables and data structures
  • Clean, readable timestamps without configuration hell
  • Smart exception formatting that actually helps you debug
  • Path visibility control based on your environment

The best part? It’s not just prettier—it’s genuinely faster to parse visually when you’re hunting for that one error in a wall of logs.

The Configuration That Actually Works

Here’s the exact setup I copy into every project. It’s opinionated, but that’s the point—consistency beats customization.

Set Up Production-Ready Logging

Configure Python logging with Rich handler and proper filtering

Install Rich

First, add Rich to your dependencies:

1
pip install rich

Or add it to your requirements.txt or pyproject.toml. Rich is stable, well-maintained, and the only external dependency you need.

Create the Configuration Dictionary

This is the core. I usually put this in a logging_config.py or at the top of my main module:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import logging
import logging.config
from os import getenv

LOGGING_CONFIG = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "rich": {"format": "%(message)s", "datefmt": "[%X]"},
    },
    "handlers": {
        "rich": {
            "class": "rich.logging.RichHandler",
            "level": "INFO",
            "formatter": "rich",
            "show_time": False,
            "rich_tracebacks": False,
            "show_path": lambda: True if getenv("API_RUNTIME") == "dev" else False,
            "tracebacks_show_locals": False,
        },
    },
    "loggers": {
        "": {  # Root logger configuration
            "level": "INFO",
            "handlers": ["rich"],
            "propagate": True,
        },
        "httpx": {
            "level": "WARNING",
            "handlers": [],
            "propagate": False,
        },
        "uvicorn.access": {
            "level": "WARNING",
            "handlers": [],
            "propagate": False,
        },
    },
}

The magic is in those logger overrides. httpx and uvicorn.access are silenced unless something actually goes wrong.

Initialize the Logger

Create a simple initialization function and call it once at startup:

1
2
3
4
5
6
7
8
LOGGER_NAME = None  # Uses root logger

def configure_logging():
    logging.config.dictConfig(LOGGING_CONFIG)
    logger = logging.getLogger(LOGGER_NAME)
    return logger

logger = configure_logging()

That’s it. Import this logger anywhere in your project and use it. No per-file configuration, no repeated setup code.

The Details That Matter

Environment-Aware Path Display

Notice this line in the config:

1
"show_path": lambda: True if getenv("API_RUNTIME") == "dev" else False,

In development, you want to see which file and line number each log came from. In production? That’s just noise. This lambda switches behavior based on your environment without any code changes.

Silencing Noisy Dependencies

Log Propagation : When propagate is False, log messages stop at that logger and don’t bubble up to parent loggers, effectively isolating that library’s logging behavior.

The httpx and uvicorn.access logger configs are crucial. These libraries are chatty by design—they log every request, every connection pool event, every retry. Great for debugging the HTTP layer, terrible for reading your application logs.

By setting their level to WARNING and propagate to False, you only see their logs when something breaks. Perfect balance.

Why Not Rich Tracebacks?

You might notice rich_tracebacks: False. Rich’s enhanced tracebacks are gorgeous, but they’re also long. In a containerized environment or CI/CD pipeline, that extra formatting can actually make logs harder to scan. I keep them off by default and enable them locally when I need to deep-dive on an exception.

When This Setup Breaks Down

This isn’t perfect for everyone. Here’s when you might need something different:

If you’re shipping libraries: Don’t configure the root logger. Let the consuming application handle that. Just get a logger with your package name.

If you need structured JSON logs: Rich is great for humans, terrible for log aggregation tools like Datadog or Splunk. You’ll want a JSON formatter instead.

If you’re running on AWS Lambda: The Lambda runtime has opinions about logging. You might need to stick closer to the defaults or use their powertools library.

If you have complex multi-process setups: You’ll need QueueHandler and QueueListener to safely log from worker processes. This config is for single-process apps or simple async setups.

The One Thing I Always Customize

The only thing I consistently change across projects is adding more noisy loggers to the silence list. Common culprits:

  • urllib3 - Similar to httpx, loves to talk about connections
  • botocore - AWS SDK logs EVERYTHING
  • asyncio - In debug mode, it’s overwhelming
  • paramiko - SSH operations get verbose fast

Just add them to the loggers dict with level WARNING or ERROR, and you’re golden.

FAQ

Why not use structlog or loguru instead?

Both are great! Structlog is perfect for structured logging in production, and loguru has an even simpler API than standard logging. But they’re also larger dependencies with different patterns. This setup uses stdlib logging with one small dependency (Rich), making it easy to adopt gradually and removing it later if needed.

How do I change log levels for different environments?

The cleanest way is environment variables. Change the INFO level in the config to os.getenv("LOG_LEVEL", "INFO") and set LOG_LEVEL=DEBUG in development. Or use separate config files loaded based on environment.

Can I use this with FastAPI or Flask?

Absolutely. Call configure_logging() before you create your app instance. FastAPI and Flask will use whatever logging config you’ve set up. Just make sure to silence their access logs in the config if you don’t need request-level logging.

What about logging to files?

Add a file handler to the handlers dict and include it in the root logger’s handlers list. I typically use RotatingFileHandler with maxBytes and backupCount for production file logging.

Why This Should Be Default

Here’s my controversial take: this configuration—or something very close to it—should be what Python ships with. Not the bare-bones plain text output we get today.

The default logging config optimizes for simplicity, which makes sense for a standard library. But in practice, every production Python project ends up building something like this anyway. We all independently discover that third-party libraries are too noisy, that colorized output helps, and that environment-specific behavior matters.

Having a blessed “production-ready” logging config in the Python docs would save thousands of developers from reinventing this wheel. Until then, I’m copy-pasting this file into every new project.

Conclusion

Key Takeaways

  • Standard Python logging works fine but needs configuration to be great for production use
  • Rich handler provides colorized, readable output with minimal setup overhead
  • Silencing noisy third-party loggers (httpx, uvicorn, boto) dramatically improves log readability
  • Environment-aware configuration through environment variables keeps the same code working in dev and prod
  • One logging config file copied across projects ensures consistency and saves setup time
  • Disable rich_tracebacks in production to keep logs scannable in containerized environments
  • This setup works best for single-process applications and async services, not multi-process workers

The best logging config is the one you never think about after the initial setup. Copy this, adjust the noisy logger list for your stack, and move on to actually building features. Your future debugging self will thank you.

Security runs on data.
Make it work for you.

Effortlessly test and evaluate web application security using Vibe Eval agents.