Python Rich Logging: Tracebacks and Syntax Highlighting
Learn how to configure RichHandler for Python logging, including readable tracebacks, syntax highlighting options, and production considerations.
When Python's standard logging output meets a long traceback, the terminal becomes a wall of plain text. The Rich library changes that by adding syntax highlighting and structured traceback formatting to logging output. This article shows how to configure Rich logging for tracebacks and syntax highlighting in your own projects.
Setting Up Rich Logging
Rich provides a RichHandler that plugs directly into Python's standard logging module. Install Rich first:
pip install rich
Then replace the default handler with RichHandler:
import logging from rich.logging import RichHandler logging.basicConfig( level=logging.INFO, handlers=[RichHandler()] ) log = logging.getLogger('example') log.info('Hello, Rich logging')
RichHandler writes log records to the console using Rich's rendering engine. The output is colorized by default: timestamps, level names, and messages get distinct colors. This alone makes logs easier to scan, but the real value appears when an exception is logged.
Traceback Formatting with Rich
When you log an exception with logger.exception() or logger.error(..., exc_info=True), RichHandler formats the traceback using Rich's Traceback class. The result is a syntax-highlighted, indented traceback with clear frame separation. Consider this example:
import logging from rich.logging import RichHandler logging.basicConfig(level=logging.INFO, handlers=[RichHandler()]) log = logging.getLogger('example') def divide(a, b): return a / b try: log.info('Starting division') divide(10, 0) except ZeroDivisionError: log.exception('Division failed')
This logs the normal info message first and then logs the exception with a Rich-formatted traceback. Each frame is colorized, the offending line is highlighted, and the error message is shown in a contrasting color. This is a direct improvement over the default traceback, which uses monochrome text and makes it hard to spot the root cause quickly.
Note that RichHandler does not hook into Python's default sys.excepthook. For uncaught exceptions outside the logging system, call rich.traceback.install() separately if you want Rich traceback formatting there too.
Syntax Highlighting in Log Messages
Rich also applies syntax highlighting inside the tracebacks it renders. For standalone code blocks, Rich's Syntax object is a renderable, but the standard logging pipeline converts message arguments to strings before a handler sees them:
# This logs a plain string; the Syntax object is converted with str() from rich.syntax import Syntax code = '''def greet(name): return f'Hello, {name}' ''' syntax = Syntax(code, 'python', theme='monokai', line_numbers=True) log.info('Generated function: %s', syntax)
So do not expect Syntax highlighting to survive when it is interpolated into a log message with %s. If you need a fully highlighted code block, render the Syntax object with a Rich Console:
from rich.console import Console from rich.syntax import Syntax console = Console() code = '''def greet(name): return f'Hello, {name}' ''' syntax = Syntax(code, 'python', theme='monokai', line_numbers=True) console.log(syntax)
The theme parameter controls the color scheme, and line_numbers adds line numbers to the output. RichHandler also has its own highlight option for coloring common tokens in ordinary log messages; set highlight=False when you want to disable that extra processing.
Controlling Traceback Detail
RichHandler exposes parameters to control how much traceback information is shown. The most useful ones are tracebacks_show_locals and tracebacks_max_frames:
import logging from rich.logging import RichHandler logging.basicConfig( level=logging.INFO, handlers=[RichHandler( tracebacks_show_locals=True, tracebacks_max_frames=5 )] )
tracebacks_show_locals=True includes the values of local variables in each frame of the traceback. This can be extremely helpful during debugging, but it may expose sensitive data in logs. tracebacks_max_frames limits how many frames are displayed, preventing extremely long tracebacks from flooding the console.
Customizing the Console and Theme
Rich's Console object controls the output stream and theme. You can pass a custom Console to RichHandler to change colors, width, or output target:
import logging from rich.console import Console from rich.logging import RichHandler from rich.theme import Theme custom_theme = Theme({ 'logging.level.info': 'cyan', 'logging.level.warning': 'yellow', 'logging.level.error': 'bold red' }) console = Console(theme=custom_theme, width=120) logging.basicConfig(level=logging.INFO, handlers=[RichHandler(console=console)])
This is useful when you want to match the log colors to your application's branding or when you need to control the console width for better readability in narrow terminals.
Performance and Production Considerations
Rich logging adds overhead compared to plain text logging. Syntax highlighting and traceback rendering require parsing and colorization, which takes CPU time. In development this is usually negligible, but in high-throughput production systems it can become noticeable.
If you need Rich's readability for warnings and errors while keeping routine info and debug records plain, use a plain handler with a filter and a separate RichHandler for higher severities:
import logging from rich.logging import RichHandler logger = logging.getLogger('app') logger.setLevel(logging.DEBUG) plain = logging.StreamHandler() plain.setLevel(logging.DEBUG) plain.addFilter(lambda record: record.levelno < logging.WARNING) rich = RichHandler(level=logging.WARNING) logger.addHandler(plain) logger.addHandler(rich)
With this configuration, debug and info records use the plain handler, while warning and error records are rendered with Rich. You can also disable the lightweight message highlighting by setting highlight=False on the RichHandler if you only need traceback formatting.
Integrating with Existing Logging Configuration
If your application already uses a logging configuration file or a custom formatter, you can replace the handler without changing the rest of the setup. RichHandler works with any Logger instance and respects the log level hierarchy. For example, if you use logging.config.dictConfig, you can specify RichHandler as the handler class:
import logging.config LOGGING_CONFIG = { 'version': 1, 'handlers': { 'rich': { 'class': 'rich.logging.RichHandler', 'level': 'INFO' } }, 'root': { 'handlers': ['rich'], 'level': 'INFO' } } logging.config.dictConfig(LOGGING_CONFIG)
Because RichHandler inherits from logging.Handler, it can be used anywhere a standard handler is expected. The only requirement is that the Rich package is installed in the environment where the logging runs.
Handling Edge Cases in Rich Logging
Rich builds tracebacks from the same exception information that Python's traceback module exposes. If that information is incomplete, for example during interpreter shutdown, the rendered traceback will be limited too. This is not usually a problem in ordinary application code.
When logging from multiple threads, the standard logging module serializes access to a single handler. If you share one Console among several handlers, output from different handlers can interleave. In most applications, a single RichHandler is sufficient; if you need strict grouping, use your own lock around log writes or route each thread to its own handler.
Finally, Rich's Syntax highlighting depends on the Pygments library. If you use a language Pygments does not support, the Syntax renderable will not highlight it unless you provide a custom lexer. In practice, the built-in lexers cover most languages you might display.