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:
users = []
print("User List:")print("-" * 60)for user in users: print(f"ID: {user['id']} | Name: {user['name']} | Email: {user['email']} | Status: {user['status']}")User List:------------------------------------------------------------ID: 1 | Name: Alice | Email: [email protected] | Status: activeID: 2 | Name: Bob | Email: [email protected] | Status: inactiveID: 3 | Name: Charlie | Email: [email protected] | Status: activeThis 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
# Green textprint("\033[92mSuccess!\033[0m")# Red textprint("\033[91mError!\033[0m")# Boldprint("\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
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)
from tqdm import tqdmimport 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.
from rich.console import Consolefrom rich.table import Table
users = []
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.
from rich.progress import trackimport time
files = [f"file_{i}.txt" for i in range(100)]
for file in track(files, description="Processing files..."): time.sleep(0.05) # Simulate processingOne 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:
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumnimport 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:
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:
from rich.console import Consolefrom 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:
from rich.console import Consolefrom 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:
from rich.console import Consolefrom rich.layout import Layoutfrom 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:
from rich.logging import RichHandlerimport 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
# WRONG: Inconsistent outputprint("Starting...")console.print("[green]Done![/green]")
# RIGHT: Use Console consistentlyconsole.print("Starting...")console.print("[green]Done![/green]")Mistake 2: Overusing Styles
# WRONG: Rainbow vomitconsole.print("[bold red on yellow blink]CRITICAL ERROR!!![/]")
# RIGHT: Use styles purposefullyconsole.print("[bold red]Error:[/] Connection failed.")Mistake 3: Ignoring Progress Feedback
# WRONG: No feedback during long operationfor item in large_list: process(item)
# RIGHT: Show progressfor item in track(large_list, description="Processing..."): process(item)Mistake 4: Creating Custom Formatters
# WRONG: Reinventing the wheeldef format_table(data): ...
# RIGHT: Use Rich's built-in Tablefrom rich.table import TableBenchmark: Code Reduction
I measured the impact on my CLI tools:
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:
# Start with Console for basic outputfrom rich.console import Consoleconsole = 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 displayedThe 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:
- Tables with zero formatting code - Rich handles alignment, borders, and styling
- Progress bars with one line - Users always know what’s happening
- Colors without escape codes - Readable markup instead of magic numbers
- Syntax highlighting built-in - No external highlighter needed
- 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