Post

Rich: I wanted to make my Terminal app Useful

Rich: I wanted to create terminal apps that were easy and intuitive for my users. Rich was the solution.

Rich: I wanted to make my Terminal app Useful

Rich: I wanted to make my Terminal app Useful

If you’ve ever stared at a wall of plain text in your terminal and thought “there has to be a better way” — there is. It’s called Rich, and it will change how you think about command-line output forever.


Who Built It?

Rich was created by Will McGugan, a British software developer, and first released in 2020. McGugan built it out of frustration with the limitations of standard terminal output. He wanted a library that felt as expressive as HTML/CSS but worked entirely in the terminal.

The project took off quickly. Today it has over 47,000 stars on GitHub, is downloaded millions of times per month, and is used by major projects like FastAPI, Textual, Pytest, and AWS CLI.

McGugan later used Rich as the foundation for Textual — a full TUI (Terminal User Interface) framework — which is essentially Rich’s spiritual successor for building complete terminal applications.


What Can Rich Do?

Rich turns your terminal into a canvas. Here’s what it brings to the table:

Styled Text

Apply colors, bold, italic, underline, strikethrough — all using simple markup tags that feel like HTML.

1
2
3
4
5
from rich import print

print("[bold magenta]Hello[/bold magenta], [green]world![/green]")
print("[red underline]Warning:[/red underline] Something went wrong.")
print("[on blue] Inverted background [/on blue]")

It supports 256 colors, True Color (16 million colors via RGB), and automatically degrades gracefully on terminals that support fewer colors.


Tables

One of Rich’s most powerful features. You define columns, add rows, and Rich handles all the alignment, borders, and formatting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from rich.console import Console
from rich.table import Table

console = Console()

table = Table(title="Top Programming Languages 2024")
table.add_column("Language", style="cyan", no_wrap=True)
table.add_column("Created By", style="magenta")
table.add_column("Year", justify="right", style="green")
table.add_column("Primary Use", style="yellow")

table.add_row("Python", "Guido van Rossum", "1991", "General purpose / Data / AI")
table.add_row("Rust", "Graydon Hoare", "2010", "Systems programming")
table.add_row("TypeScript", "Anders Hejlsberg", "2012", "Web development")
table.add_row("Go", "Google", "2009", "Cloud / Backend")

console.print(table)

Tables support column alignment, custom styles per column, custom border styles, and even nested content.


Progress Bars

Track long-running tasks with animated progress bars — either simple one-liners or complex multi-task views.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from rich.progress import track
import time

# The easy way — wrap any iterable
for item in track(my_list, description="Processing..."):
    process(item)

# The powerful way — multiple concurrent tasks
from rich.progress import Progress

with Progress() as progress:
    task1 = progress.add_task("[cyan]Downloading...", total=1000)
    task2 = progress.add_task("[magenta]Processing...", total=500)

    while not progress.finished:
        progress.update(task1, advance=10)
        progress.update(task2, advance=5)
        time.sleep(0.02)

Panels and Layout

Wrap content in bordered panels, display things in columns, or build full split-screen layouts.

1
2
3
4
5
6
7
8
9
10
11
12
from rich.panel import Panel
from rich.columns import Columns
from rich.console import Console

console = Console()

# A simple panel
console.print(Panel("This is inside a panel", title="Info", border_style="blue"))

# Side-by-side columns
panels = [Panel(f"Item {i}", expand=True) for i in range(1, 4)]
console.print(Columns(panels))

Syntax Highlighting

Display code with full syntax highlighting for dozens of languages — useful for developer tools and REPLs.

1
2
3
4
5
6
7
8
9
10
from rich.syntax import Syntax
from rich.console import Console

console = Console()
code = '''
def greet(name: str) -> str:
    return f"Hello, {name}!"
'''
syntax = Syntax(code, "python", theme="monokai", line_numbers=True)
console.print(syntax)

Logging Integration

Drop-in replacement for Python’s standard logging — adds timestamps, log levels in color, and source file context with a single line.

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging
from rich.logging import RichHandler

logging.basicConfig(
    level=logging.DEBUG,
    format="%(message)s",
    handlers=[RichHandler()]
)

log = logging.getLogger("myapp")
log.info("Server started on port 8080")
log.warning("Memory usage at 85%")
log.error("Database connection failed")

inspect() — Object Explorer

A visual alternative to Python’s help(). Prints a clean, color-coded summary of any object’s attributes and methods.

1
2
3
4
5
from rich import inspect

inspect([])            # Inspect a list
inspect(str)           # Inspect a class
inspect(os, methods=True)  # Inspect a module with all methods

Building an Interactive Table App

Here’s a complete, runnable app that displays a movie database in a table and lets the user filter, sort, and explore it interactively.

The app uses:

  • Rich for all visual output
  • Python’s built-in input() for interaction
  • No external dependencies beyond Rich
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# movie_explorer.py
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.prompt import Prompt, Confirm
from rich.text import Text
from rich import box

console = Console()

# --- Data ---
MOVIES = [
    {"title": "Inception",        "year": 2010, "genre": "Sci-Fi",   "director": "Christopher Nolan", "rating": 8.8},
    {"title": "The Godfather",    "year": 1972, "genre": "Crime",    "director": "Francis Ford Coppola", "rating": 9.2},
    {"title": "Interstellar",     "year": 2014, "genre": "Sci-Fi",   "director": "Christopher Nolan", "rating": 8.6},
    {"title": "Parasite",         "year": 2019, "genre": "Thriller", "director": "Bong Joon-ho",      "rating": 8.5},
    {"title": "The Dark Knight",  "year": 2008, "genre": "Action",   "director": "Christopher Nolan", "rating": 9.0},
    {"title": "Pulp Fiction",     "year": 1994, "genre": "Crime",    "director": "Quentin Tarantino", "rating": 8.9},
    {"title": "Dune",             "year": 2021, "genre": "Sci-Fi",   "director": "Denis Villeneuve",  "rating": 8.0},
    {"title": "Whiplash",         "year": 2014, "genre": "Drama",    "director": "Damien Chazelle",   "rating": 8.5},
    {"title": "Get Out",          "year": 2017, "genre": "Horror",   "director": "Jordan Peele",      "rating": 7.7},
    {"title": "Everything Everywhere All at Once", "year": 2022, "genre": "Sci-Fi", "director": "Daniels", "rating": 7.8},
]

GENRE_COLORS = {
    "Sci-Fi":   "cyan",
    "Crime":    "red",
    "Thriller": "magenta",
    "Action":   "yellow",
    "Drama":    "blue",
    "Horror":   "dark_red",
}


def rating_bar(rating: float) -> Text:
    """Turn a numeric rating into a colored visual bar."""
    filled = int(rating)
    bar = "" * filled + "" * (10 - filled)
    color = "green" if rating >= 8.5 else "yellow" if rating >= 7.5 else "red"
    return Text(f"{bar} {rating}", style=color)


def build_table(movies: list, title: str = "Movie Explorer") -> Table:
    """Build and return a Rich Table from a list of movie dicts."""
    table = Table(
        title=title,
        box=box.ROUNDED,
        border_style="bright_black",
        header_style="bold white on dark_blue",
        show_lines=True,
    )

    table.add_column("#",         style="dim",     width=4,  justify="right")
    table.add_column("Title",     style="bold",    min_width=20)
    table.add_column("Year",      justify="center", width=6)
    table.add_column("Genre",     width=10)
    table.add_column("Director",  style="italic",  min_width=18)
    table.add_column("Rating",    min_width=18)

    for i, movie in enumerate(movies, 1):
        genre = movie["genre"]
        genre_color = GENRE_COLORS.get(genre, "white")
        genre_text = Text(genre, style=f"bold {genre_color}")

        table.add_row(
            str(i),
            movie["title"],
            str(movie["year"]),
            genre_text,
            movie["director"],
            rating_bar(movie["rating"]),
        )

    return table


def show_menu() -> None:
    """Print the main menu."""
    console.print(Panel(
        "[1] Browse all movies\n"
        "[2] Filter by genre\n"
        "[3] Filter by director\n"
        "[4] Sort by rating\n"
        "[5] Sort by year\n"
        "[6] Search by title\n"
        "[q] Quit",
        title="[bold cyan]Main Menu[/bold cyan]",
        border_style="cyan",
        expand=False,
    ))


def filter_by_genre(movies: list) -> list:
    genres = sorted(set(m["genre"] for m in movies))
    console.print("\nAvailable genres: " + ", ".join(f"[bold]{g}[/bold]" for g in genres))
    choice = Prompt.ask("Enter genre")
    return [m for m in movies if m["genre"].lower() == choice.lower()]


def filter_by_director(movies: list) -> list:
    choice = Prompt.ask("Enter director name (partial match ok)")
    return [m for m in movies if choice.lower() in m["director"].lower()]


def search_by_title(movies: list) -> list:
    query = Prompt.ask("Enter title to search")
    return [m for m in movies if query.lower() in m["title"].lower()]


def main() -> None:
    console.clear()
    console.print(Panel(
        "[bold white]Welcome to[/bold white] [bold magenta]Movie Explorer[/bold magenta]\n"
        "[dim]Powered by Rich[/dim]",
        border_style="magenta",
        expand=False,
    ))

    movies = MOVIES.copy()

    while True:
        console.print()
        show_menu()
        console.print()

        choice = Prompt.ask("[bold]Your choice[/bold]", choices=["1","2","3","4","5","6","q"])

        console.print()

        if choice == "q":
            console.print("[bold green]Goodbye! 🎬[/bold green]")
            break

        elif choice == "1":
            table = build_table(movies, f"All Movies ({len(movies)} total)")
            console.print(table)

        elif choice == "2":
            filtered = filter_by_genre(movies)
            if filtered:
                console.print(build_table(filtered, f"Genre Filter — {len(filtered)} results"))
            else:
                console.print("[red]No movies found for that genre.[/red]")

        elif choice == "3":
            filtered = filter_by_director(movies)
            if filtered:
                console.print(build_table(filtered, f"Director Filter — {len(filtered)} results"))
            else:
                console.print("[red]No movies found for that director.[/red]")

        elif choice == "4":
            sorted_movies = sorted(movies, key=lambda m: m["rating"], reverse=True)
            console.print(build_table(sorted_movies, "Sorted by Rating (highest first)"))

        elif choice == "5":
            sorted_movies = sorted(movies, key=lambda m: m["year"], reverse=True)
            console.print(build_table(sorted_movies, "Sorted by Year (newest first)"))

        elif choice == "6":
            results = search_by_title(movies)
            if results:
                console.print(build_table(results, f"Search Results — {len(results)} found"))
            else:
                console.print("[red]No movies matched your search.[/red]")

        console.print()
        if not Confirm.ask("Back to menu?", default=True):
            console.print("[bold green]Goodbye! 🎬[/bold green]")
            break


if __name__ == "__main__":
    main()

Running the App

1
2
pip install rich
python movie_explorer.py

Key Concepts from the App

ConceptWhat it does
Console()The central object — controls output, width, color support
TableDefines structure; each column gets its own style and alignment
TextA styled string you can build programmatically and embed anywhere
Prompt.ask()Asks the user for input, with optional validation and choices
Confirm.ask()Asks a yes/no question
PanelWraps any content in a border with an optional title
box.ROUNDEDOne of many border styles — others include SIMPLE, HEAVY, DOUBLE
console.clear()Clears the terminal, useful for refreshing the view

Tips and Tricks

Export to HTML or SVG — Rich can save its output as a static file:

1
2
3
4
console = Console(record=True)
console.print(table)
console.save_html("output.html")
console.save_svg("output.svg")

Disable color for CI/logs — Rich detects whether it’s running in a terminal automatically. You can also force it:

1
2
console = Console(no_color=True)   # Plain text
console = Console(force_terminal=True)  # Always colored

Markdown rendering — Rich can render Markdown directly in the terminal:

1
2
from rich.markdown import Markdown
console.print(Markdown("# Hello\n\nThis is **bold** and this is `code`."))

Final Thoughts

Rich is one of those rare libraries that improves your workflow the moment you install it. Whether you’re building dev tools, data pipelines, CLI apps, or just want better debug output, Rich makes the terminal feel like a first-class environment — not an afterthought.

The interactive table app above is just a starting point. From here you could add pagination, export options, live-updating views with Live, or even migrate to Textual when you need mouse support and full TUI widgets.

GitHub: github.com/Textualize/rich
Docs: rich.readthedocs.io

This post is licensed under CC BY 4.0 by the author.