Skip to content

How to Make Python CLI Output Beautiful with Rich

Problem

I built a CLI tool that processed user data and displayed results in the terminal. The output was a mess. Long lines wrapped awkwardly, tables were misaligned, and important information got lost in a sea of flat text.

The typical approach:

ugly_output.py
users = [
{"id": 1, "name": "Alice", "email": "[email protected]", "status": "active"},
{"id": 2, "name": "Bob", "email": "[email protected]", "status": "inactive"},
{"id": 3, "name": "Charlie", "email": "[email protected]", "status": "active"},
]
print("User List:")
print("-" * 60)
for user in users:
print(f"ID: {user['id']} | Name: {user['name']} | Email: {user['email']} | Status: {user['status']}")
terminal_output.txt
User List:
------------------------------------------------------------
ID: 1 | Name: Alice | Email: [email protected] | Status: active
ID: 2 | Name: Bob | Email: [email protected] | Status: inactive
ID: 3 | Name: Charlie | Email: [email protected] | Status: active

This worked, but it looked amateurish. When I added more columns or longer email addresses, the alignment broke. When I ran long operations, users had no idea if the script was still running or frozen.

What Didn’t Work

Before finding Rich, I tried several approaches to make output better.

Attempt 1: ANSI Escape Codes

ansi_colors.py
# Green text
print("\033[92mSuccess!\033[0m")
# Red text
print("\033[91mError!\033[0m")
# Bold
print("\033[1mImportant\033[0m")

This worked for basic colors but quickly became unreadable. I kept forgetting the codes, and different terminals handled them differently. Plus, it did nothing for tables or progress bars.

Attempt 2: String Formatting for Tables

manual_table.py
def print_table(data, headers):
# Calculate column widths
widths = [len(h) for h in headers]
for row in data:
for i, cell in enumerate(row):
widths[i] = max(widths[i], len(str(cell)))
# Print header
header_row = " | ".join(h.ljust(widths[i]) for i, h in enumerate(headers))
print(header_row)
print("-" * len(header_row))
# Print rows
for row in data:
print(" | ".join(str(cell).ljust(widths[i]) for i, cell in enumerate(row)))

I wrote and rewrote this code dozens of times. It handled basic cases but broke on Unicode characters, didn’t support colors, and was tedious to customize.

Attempt 3: Progress Bars with Tqdm (Only Partially)

tqdm_only.py
from tqdm import tqdm
import time
for i in tqdm(range(100)):
time.sleep(0.02)

Tqdm was great for progress bars, but I still needed separate solutions for tables, colors, and structured output. My imports were growing.

The Solution: Rich

I discovered Rich when searching for a better way to display API responses. Within minutes, my ugly output transformed into something professional.

rich_basic.py
from rich.console import Console
from rich.table import Table
users = [
{"id": 1, "name": "Alice", "email": "[email protected]", "status": "active"},
{"id": 2, "name": "Bob", "email": "[email protected]", "status": "inactive"},
{"id": 3, "name": "Charlie", "email": "[email protected]", "status": "active"},
]
console = Console()
table = Table(title="User List")
table.add_column("ID", style="cyan", justify="center")
table.add_column("Name", style="magenta")
table.add_column("Email", style="green")
table.add_column("Status", style="bold yellow")
for user in users:
status_style = "green" if user["status"] == "active" else "red"
table.add_row(
str(user["id"]),
user["name"],
user["email"],
f"[{status_style}]{user['status']}[/{status_style}]"
)
console.print(table)

The output was immediately readable with colors, alignment, and a clean table format. No manual width calculations. No ANSI code memorization.

Progress Bars Made Easy

I had a script that processed thousands of files. Users couldn’t tell if it was running or frozen.

rich_progress.py
from rich.progress import track
import time
files = [f"file_{i}.txt" for i in range(100)]
for file in track(files, description="Processing files..."):
time.sleep(0.05) # Simulate processing

One line of code. That’s all it took to add a progress bar with ETA, completed count, and elapsed time.

For more control, I used the Progress class:

rich_progress_advanced.py
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
import time
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
BarColumn(),
TextColumn("[progress.percentage]{task.percentage:>3.0f}%"),
) as progress:
task1 = progress.add_task("[red]Downloading...", total=100)
task2 = progress.add_task("[green]Processing...", total=100)
task3 = progress.add_task("[cyan]Uploading...", total=100)
while not progress.finished:
progress.update(task1, advance=0.5)
progress.update(task2, advance=0.3)
progress.update(task3, advance=0.9)
time.sleep(0.02)

This showed multiple simultaneous operations, each with its own bar. Perfect for parallel tasks.

Console Markup for Quick Styling

When I just needed colored output without tables, Rich’s console markup was the answer:

rich_markup.py
from rich.console import Console
console = Console()
console.print("[bold green]Success![/bold green] User created successfully.")
console.print("[bold red]Error![/bold red] Connection failed.")
console.print("[yellow]Warning:[/yellow] Rate limit approaching.")
console.print("[bold]Important:[/bold] Please read the [link=https://example.com]documentation[/link].")

The markup syntax was intuitive: [style]text[/style]. I could nest styles, add links, and even print emojis.

Syntax Highlighting for Code

I built a tool that displayed code snippets to users. Rich handled syntax highlighting automatically:

rich_syntax.py
from rich.console import Console
from rich.syntax import Syntax
console = Console()
code = '''
def calculate_total(items):
return sum(item["price"] * item["quantity"] for item in items)
'''
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)

No external dependencies for highlighting. It just worked.

Panels and Layouts for Structure

When I needed to display multiple pieces of information, panels kept things organized:

rich_panels.py
from rich.console import Console
from rich.panel import Panel
console = Console()
console.print(Panel("[bold green]Build Passed[/bold green]", title="Status", border_style="green"))
console.print(Panel("[bold red]Tests Failed[/bold red]\n3 of 50 tests failed.", title="Results", border_style="red"))

For complex CLI dashboards, the Layout system let me create split-screen views:

rich_layout.py
from rich.console import Console
from rich.layout import Layout
from rich.panel import Panel
console = Console()
layout = Layout()
layout.split(
Layout(name="header", size=3),
Layout(name="body", ratio=1),
Layout(name="footer", size=3),
)
layout["header"].update(Panel("Application Status Dashboard"))
layout["body"].update(Panel("Main content area"))
layout["footer"].update(Panel("Press Q to quit"))
console.print(layout)

Logging Integration

I replaced my print-based debugging with Rich’s logging:

rich_logging.py
from rich.logging import RichHandler
import logging
logging.basicConfig(
level="INFO",
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler()]
)
log = logging.getLogger("rich")
log.info("Starting application...")
log.warning("Low disk space detected")
log.error("Failed to connect to database")

The output was colored, timestamped, and formatted with proper log levels.

Common Mistakes to Avoid

Mistake 1: Mixing Print and Console

mixed_output.py
# WRONG: Inconsistent output
print("Starting...")
console.print("[green]Done![/green]")
# RIGHT: Use Console consistently
console.print("Starting...")
console.print("[green]Done![/green]")

Mistake 2: Overusing Styles

overstyled.py
# WRONG: Rainbow vomit
console.print("[bold red on yellow blink]CRITICAL ERROR!!![/]")
# RIGHT: Use styles purposefully
console.print("[bold red]Error:[/] Connection failed.")

Mistake 3: Ignoring Progress Feedback

no_feedback.py
# WRONG: No feedback during long operation
for item in large_list:
process(item)
# RIGHT: Show progress
for item in track(large_list, description="Processing..."):
process(item)

Mistake 4: Creating Custom Formatters

custom_formatter.py
# WRONG: Reinventing the wheel
def format_table(data): ...
# RIGHT: Use Rich's built-in Table
from rich.table import Table

Benchmark: Code Reduction

I measured the impact on my CLI tools:

code_comparison.txt
Feature Before Rich After Rich Reduction
----------------------------------------------------------
Table formatting 45 lines 10 lines 78%
Colored output 20 lines 5 lines 75%
Progress tracking 30 lines 3 lines 90%
Syntax highlighting 50 lines 5 lines 90%
----------------------------------------------------------
Total 145 lines 23 lines 84%

The code was shorter and more readable. Maintenance became easier because Rich handled edge cases I hadn’t considered.

When Not to Use Rich

Rich isn’t always the right choice:

  • Scripts that must run in minimal environments (embedded systems, CI runners without color support)
  • Output piped to other commands (Rich output can interfere with parsing)
  • Logs destined for text files (colors and formatting add noise)
  • Projects with strict dependency limits

For those cases, I fall back to plain print or standard logging.

Integration Tips

I added Rich to my existing projects incrementally:

gradual_adoption.py
# Start with Console for basic output
from rich.console import Console
console = Console()
# Replace print statements one by one
# print(f"Error: {msg}") becomes:
console.print(f"[bold red]Error:[/] {msg}")
# Add tables where output is dense
# Add progress bars where operations are slow
# Add syntax highlighting where code is displayed

The library was designed for gradual adoption. I could use as much or as little as needed.

Conclusion

Rich transformed my CLI tools from amateurish scripts into polished utilities. The key wins:

  1. Tables with zero formatting code - Rich handles alignment, borders, and styling
  2. Progress bars with one line - Users always know what’s happening
  3. Colors without escape codes - Readable markup instead of magic numbers
  4. Syntax highlighting built-in - No external highlighter needed
  5. Cross-platform consistency - Works in every terminal I’ve tested

I started using Rich just to make output “less ugly” and ended up using it everywhere. Tables, progress bars, logging, syntax highlighting - once I had the library in place, it was easy to keep finding uses for it.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments