Geek Tech
GeekTech

GeekTech

The terminal formatting library you need in 2022

The terminal formatting library you need in 2022

Geek Tech's photo
Geek Tech
·Feb 27, 2022·

16 min read

Subscribe to my newsletter and never miss my upcoming articles

Table of contents

When designing CLI applications we always face challenges as python programmers when it comes to making the text/UI look good. But have you ever wondered if this tedious task could be abstracted behind a powerful terminal UI library, which is simple to use yet extremely powerful? Well, you're in luck as this blog will tell you all you need to know about such a library: Rich.

Rich is a Python library for rich text and gorgeous formatting in the console. Rich makes it easy to add color and style to the CLI of your program. It supports rendering tables, syntax highlighted source code, progress bars, markdown, tracebacks and many more out of the box!

Compatibility

Rich is cross-platform and also works with the True Color / Emojis from the new Windows Terminal. The classic terminal in Windows is limited to 16 colours. Rich requires Python 3.6.1 or higher. It also works with Jupyter notebooks without any additional configuration.

Installation

Rich can be installed through from PyPI very easily using pip (or any other package manager).

python -m pip install rich

Then you can test Rich using the following command:

python -m rich

rich sample
output

Getting started

Rich has a plethora of features under its belt. Let's have a quick look at each of them.

Rich Print

Rich provides a print() function to facilitate printing rich output to the console. Moreover, this function has an identical signature to the built-in print(). Let's try it!

from rich import print

# Colored text
print("[yellow]Yellow text[/yellow]")

# Stylized text
print("[bold]Bold text[/bold]")

# Emojis
print("Text with :lemon:")

# All three combined
print("[bold yellow]Bold yellow text with :lemon:[/bold yellow]")

# Pretty formatting of objects
print(locals())

Output:

rich print output

Rich supports Console Markup (inspired by bbcode) to insert colour and stylize the output. You can then print strings or objects to the terminal in the usual way. Rich will do some basic syntax highlighting and format data structures to make them easier to read.

If you don't want to replace the built-in print() in your program with Rich's you can use an import alias.

from rich import print as rprint

Console Markup

Rich supports a simple markup which you can use to insert colour and styles virtually everywhere Rich would accept a string (e.g. print() and log()).

Run the following command to see some examples:

python -m rich.markup

Syntax

Rich's console markup is inspired by bbcode. You can start the style by writing it in []. The style will be applied till the closing [/]. Let's see an example:

from rich import print

print("[bold red]alert![/bold red] Something happened")

Output:

output

Text

Rich has a Text class you can use to mark up strings with color and style attributes. You can use a Text instance anywhere a string is accepted, which gives you a lot of control over presentation.

One way to add a style to Text is the stylize() method which applies a style to a start and end offset. Here's an example:

from rich.console import Console
from rich.text import Text

console = Console()
text = Text("Hello, World!")
text.stylize("bold magenta", 0, 6)
console.print(text)

This will print “Hello, World!” to the terminal, with the first word in bold magenta.

Alternatively, you can construct styled text by calling append() to add a string and style to the end of the Text. Here’s an example:

text = Text()
text.append("Hello", style="bold magenta")
text.append(" World!")
console.print(text)

Text attributes

You can set several parameters on the Text class's constructor to control how the text is displayed.

  • justify should be “left”, “centre”, “right”, or “full”, and will override default justify behaviour.
  • overflow should be “fold”, “crop”, or “ellipsis”, and will override the default overflow.
  • no_wrap prevents wrapping if the text is longer than the available width.
  • tab_size Sets the number of characters in a tab.

A Text instance may be used in place of a plain string virtually everywhere in the Rich API, which gives you a lot of control over how text renders within other Rich renderables. For instance, the following example right aligns text within a Panel:

from rich import print
from rich.panel import Panel
from rich.text import Text

panel = Panel(Text("Hello", justify="right"))
print(panel)

Output:

output

Highlighting

Rich can apply styles to text patterns that you print() or log(). Rich will highlight numbers, strings, collections, booleans, None, and a few more exotic patterns like file paths, URLs, and UUIDs with the default settings.

Setting highlight=False on print() or log() disables highlighting, as does setting highlight=False on the Console constructor, which disables it everywhere. If you disable highlighting in the constructor, you may still use highlight=True on print/log to selectively enable it.

Custom Highlighters

You can create a custom highlighter if the default highlighting does not meet your needs. Extending the RegexHighlighter class, which applies a style to any text matching a list of regular expressions, is the simplest method to do this.

Here is an example of text that appears to be an email address:

from rich.console import Console
from rich.highlighter import RegexHighlighter
from rich.theme import Theme


class EmailHighlighter(RegexHighlighter):
    """Apply style to anything that looks like an email."""

    base_style = "example."
    highlights = [r"(?P<email>[\w-]+@([\w-]+\.)+[\w-]+)"]


theme = Theme({"example.email": "bold magenta"})
console = Console(highlighter=EmailHighlighter(), theme=theme)
console.print("Send funds to money@example.org")

Output:

output

While RegexHighlighter is a powerful tool, you can modify its base class Highlighter to create your highlighting scheme. It just has one method, highlight, to which the Text to highlight is supplied.

Here's an example that uses a different colour for each character:

from random import randint

from rich import print
from rich.highlighter import Highlighter


class RainbowHighlighter(Highlighter):
    def highlight(self, text):
        for index in range(len(text)):
            text.stylize(f"color({randint(16, 255)})", index, index + 1)


rainbow = RainbowHighlighter()
print(rainbow("I must not fear. Fear is the mind-killer."))

Output:

output

Pretty Printing

Rich will format (i.e. pretty print) containers like lists, dicts, and sets in addition to syntax highlighting.

To view an example of nice printed output, use the following command:

python -m rich.pretty

Take note of how the output will adjust to fit the terminal width.

pprint method

You can use the pprint() method to adjust how items are nicely printed with a few more options. Here's how you'd go about doing it:

from rich.pretty import pprint

pprint(locals())

Output:

output

Pretty renderable

You can use Rich's Pretty class to inject pretty printed data into another renderable.

The following example shows how to display attractive printed data in a basic panel:

from rich import print
from rich.pretty import Pretty
from rich.panel import Panel

pretty = Pretty(locals())
panel = Panel(pretty)
print(panel)

output

You can checkout the Rich Repr Protocol to customize Rich's formatting capabilities.

Logging Handler

Rich includes a logging handler that formats and colors text generated by the Python logging package.

An example of how to set up a rich logger is as follows:

import logging
from rich.logging import RichHandler

FORMAT = "%(message)s"
logging.basicConfig(
    level="NOTSET", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()]
)

log = logging.getLogger("rich")
log.info("Hello, World!")

Output:

output

Because most libraries are unaware of the need to escape literal square brackets, rich logs will not render Console Markup in logging by default, but you can enable it by setting markup=True on the handler. Alternatively, you can enable it per log message by adding the following extra argument:

log.error("[bold red blink]Server is shutting down![/]", extra={"markup": True})

Similarly, the highlighter can be turned off or on for each log message:

log.error("123 will not be highlighted", extra={"highlighter": None})

Handle exceptions

RichHandler can be configured to format exceptions using Rich's Traceback class, which provides more context than a built-in exception. Set rich tracebacks=True on the handler constructor to obtain attractive exceptions in your logs:

import logging
from rich.logging import RichHandler

logging.basicConfig(
    level="NOTSET",
    format="%(message)s",
    datefmt="[%X]",
    handlers=[RichHandler(rich_tracebacks=True)],
)

log = logging.getLogger("rich")
try:
    print(1 / 0)
except Exception:
    log.exception("unable print!")

Output:

output

There are a few more options for configuring logging output; check the RichHandler reference for more information.

Traceback

Rich can format and highlight Python tracebacks with syntax highlighting. Rich tracebacks are easier to read than ordinary Python tracebacks and show more code.

python -m rich.traceback

Printing tracebacks

The print_exception() method prints a traceback for the currently handled exception. Here's an illustration:

from rich.console import Console

console = Console()

try:
    do_something()
except Exception:
    console.print_exception(show_locals=True)

Output:

output

Rich displays the value of local variables for each frame of the traceback when the show_locals argument is set to True.

For a more detailed example, see exception.py.

Traceback Handler

Rich can be set as the default traceback handler, which means that all uncaught exceptions will be highlighted.

from rich.traceback import install

install(show_locals=True)

For more details on this, see Traceback Handler.

Prompt

Rich offers several Prompt classes that ask for input from the user and loop until it receives a valid response (they all use the Console API internally). Here's a simple illustration:

from rich.prompt import Prompt

name = Prompt.ask("Enter your name")

The prompt can be specified as a string (which can include Console Markup and emoji code) or as a Text object.

You can specify a default value to be returned if the user presses return without typing anything:

from rich.prompt import Prompt

name = Prompt.ask("Enter your name", default="Paul Atreides")

If you provide a list of options, the prompt will loop until the user selects one:

from rich.prompt import Prompt

name = Prompt.ask(
    "Enter your name", choices=["Paul", "Jessica", "Duncan"], default="Paul"
)

You can use IntPrompt, which asks for an integer, and FloatPrompt, which asks for floats, in addition to Prompt, which delivers strings.

The Confirm class is a specific prompt for asking a basic yes/no question to the user. Here's an example:

from rich.prompt import Confirm

is_rich_great = Confirm.ask("Do you like rich?")
assert is_rich_great

The Prompt class can be customized via inheritance. Examples can be found in prompt.py.

Run the following command from the command line to see some of the prompts in action:

python -m rich.prompt

Columns

The Columns class allows Rich to render text or other Rich renderable in clean columns. To use, create a Columns instance and print it to the Console with an iterable of renderable.

The following is a very rudimentary clone of the ls command in OSX/Linux for listing directory contents:

import os
import sys

from rich import print
from rich.columns import Columns

if len(sys.argv) < 2:
    print("Usage: python columns.py DIRECTORY")
else:
    directory = os.listdir(sys.argv[1])
    columns = Columns(directory, equal=True, expand=True)
    print(columns)

Output:

output

See columns.py for an example of a script that generates columns with more than just text.

Render Groups

The Group class enables you to group many renderables so that they can be rendered in a context where only one renderable is allowed. For example, you might want to use a Panel to display multiple renderable.

To render two panels within a third, create a Group and pass the child renderables as positional parameters, then wrap the result in another Panel:

from rich import print
from rich.console import Group
from rich.panel import Panel

panel_group = Group(
    Panel("Hello", style="on blue"),
    Panel("World", style="on red"),
)
print(Panel(panel_group))

Output:

output

This method is useful when you know ahead of time which renderables will be in a group, but it can become inconvenient if you have a higher number of renderable, particularly if they are dynamic. To cope with these circumstances, Rich provides the group() decorator. An iterator of renderables is used by the decorator to create a group.

The following is the decorator equivalent of the preceding example:

from rich import print
from rich.console import group
from rich.panel import Panel


@group()
def get_panels():
    yield Panel("Hello", style="on blue")
    yield Panel("World", style="on red")


print(Panel(get_panels()))

Markdown

Rich can render Markdown to the console. Construct a Markdown object and then print it to the console to render markdown. Markdown is a fantastic method to add rich content to your command line programmes.

Here's an example of how to put it to use:

from rich.console import Console
from rich.markdown import Markdown

MARKDOWN = """
# This is an h1

Rich can do a pretty *decent* job of rendering markdown.

1. This is a list item
2. This is another list item
"""

console = Console()
md = Markdown(MARKDOWN)
console.print(md)

Output:

output

It's worth noting that code blocks include full syntax highlighting!

The Markdown class can also be used from the command line. In the terminal, the following example displays a readme:

python -m rich.markdown README.md

To display the whole set of arguments for the markdown command, type:

python -m rich.markdown -h

Padding

To put white space around text or other renderable, use the Padding class. The following example will print the word "Hello" with 1 character of padding above and below it, as well as a space on the left and right edges:

from rich import print
from rich.padding import Padding

test = Padding("Hello", 1)
print(test)

Instead of a single value, you can describe the padding on a more detailed level by using a tuple of values. The top/bottom and left/right padding are set by a tuple of two values, whereas the padding for the top, right, bottom, and left sides is set by a tuple of four values. If you're familiar with CSS, you'll know this scheme.

The following, for example, has two blank lines above and below the text, as well as four spaces of padding on the left and right sides:

from rich import print
from rich.padding import Padding

test = Padding("Hello", 1)
print(test)

The Padding class also has a style argument that applies a style to the padding and contents, as well as an expand switch that can be set to False to prevent the padding from stretching to the terminal's full width.

Here's an example that exemplifies both of these points:

from rich import print
from rich.padding import Padding

test = Padding("Hello", (2, 4), style="on blue", expand=False)
print(test)

Padding can be used in any context, just like all Rich renderable. For example, in a Table, you could add a Padding object to a row with padding of 1 and a style of "on the red" to emphasise an item.

Padding

Construct a Panel using the renderable as the first positional argument to build a border around text or another renderable. Here's an illustration:

from rich import print
from rich.panel import Panel

print(Panel("Hello, [red]World!"))

Output:

output

By passing the box argument to the Panel constructor, you can change the panel's style. A list of possible box styles may be found at Box.

The panels will span the whole width of the terminal. By specifying expand=False on the constructor, or by building the Panel with fit(), you may make the panel fit the content. Consider the following scenario:

from rich import print
from rich.panel import Panel

print(Panel.fit("Hello, [red]World!"))

Output:

output

The Panel constructor takes two arguments: a title argument that draws a title at the top of the panel, and a subtitle argument that draws a subtitle at the bottom:

from rich import print
from rich.panel import Panel

print(Panel("Hello, [red]World!", title="Welcome", subtitle="Thank you"))

Output:

output

See Panel for details on how to customize Panels.

Progress Display

Rich can show continuously updated information about the status of long-running tasks, file copies, and so forth. The information presented can be customised; by default, a description of the 'task,' a progress bar, percentage complete, and anticipated time left will be provided.

Multiple tasks are supported with a rich progress display, each with a bar and progress statistics. This can be used to keep track of several jobs that are being worked on in threads or processes.

Try this from the command line to see how the progress display looks:

python -m rich.progress

Progress is compatible with Jupyter notebooks, however, auto-refresh is disabled. When calling update, you must explicitly call refresh() or set refresh=True when calling update(). Alternatively, you can use the track() function, which performs an automatic refresh after each loop.

Basic usage

For basic functionality, use the track() function, which takes a sequence (such as a list or range object) and an optional job description. On each iteration, the track method will return values from the sequence and update the progress information.

Here's an illustration:

from rich.progress import track
from time import sleep

for n in track(range(10), description="Processing..."):
    sleep(n)

Output:

output

For advanced usage, read the docs.

Syntax

Rich can syntax highlight various programming languages with line numbers.

Construct a Syntax object and print it to the console to highlight code. Here's an illustration:

from rich.console import Console
from rich.syntax import Syntax

console = Console()
with open("syntax.py", "rt") as code_file:
    syntax = Syntax(code_file.read(), "python")
console.print(syntax)

Output:

output

You may also use the from_path() alternative constructor which will load the code from disk and auto-detect the file type.

The example above could be re-written as follows:

from rich.console import Console
from rich.syntax import Syntax

console = Console()
syntax = Syntax.from_path("syntax.py")
console.print(syntax)

For more details, and features: read the docs

Tables

Rich's Table class provides several options for displaying tabular data on the terminal.

To draw a table, create a Table object, use add_column() and add_row() to add columns and rows, and then print it to the terminal.

Here's an illustration:

from rich.console import Console
from rich.table import Table

table = Table(title="Star Wars Movies")

table.add_column("Released", justify="right", style="cyan", no_wrap=True)
table.add_column("Title", style="magenta")
table.add_column("Box Office", justify="right", style="green")

table.add_row("Dec 20, 2019", "Star Wars: The Rise of Skywalker", "$952,110,690")
table.add_row("May 25, 2018", "Solo: A Star Wars Story", "$393,151,347")
table.add_row("Dec 15, 2017", "Star Wars Ep. V111: The Last Jedi", "$1,332,539,889")
table.add_row("Dec 16, 2016", "Rogue One: A Star Wars Story", "$1,332,439,889")

console = Console()
console.print(table)

Output:

output

Rich will compute the best column sizes for your material, and text will be wrapped to fit if the terminal isn't big enough.

The add row method allows you to do more than just add text. You are free to include everything Rich is capable of rendering (including another table).

For more details, read the docs

Tree

Rich offers a Tree class that can build a terminal tree view. A tree view is an excellent technique to display the contents of a file system or other hierarchical data. A label for each branch of the tree can be text or any other Rich renderable.

To see an example of a Rich tree, run the following command:

python -m rich.tree

The following code creates and prints a tree with a simple text label:

from rich.tree import Tree
from rich import print

tree = Tree("Rich Tree")
print(tree)

This will only output the word "Rich Tree" if there is just one Tree instance. When we call add() to add new branches to the Tree, things get much more fascinating. The code that follows adds two more branches:

tree.add("foo")
tree.add("bar")
print(tree)

Two branches will now be attached to the original tree via guide lines.

A new Tree instance is returned when you call add(). You can use this instance to create a more complex tree by adding more branches. Let's expand the tree with a couple more levels:

baz_tree = tree.add("baz")
baz_tree.add("[red]Red").add("[green]Green").add("[blue]Blue")
print(tree)

Tree styles

You can supply a style parameter for the entire branch, as well as a guide_style argument for the guidelines, in the Tree constructor and add() method. These styles are passed down through the branches and will apply to any sub-trees.

Rich will select the thicker forms of Unicode line characters if you set guide style to bold. Similarly, if you choose the "underline2" style, you'll get Unicode characters with two lines.

Examples

See tree.py, which can produce a tree view of a directory on your hard drive, for a more practical demonstration.

Live Display

To animate portions of the terminal, progress bars and status indicators employ a live display. The Live class allows you to create bespoke live displays.

Run the following command to see a live display demonstration:

python -m rich.live

Note: If you see ellipsis "...", it means your terminal isn't tall enough to display the entire table.

Basic usage

Construct a Live object with a renderable and use it as a context manager to make a live display. The live display will be visible throughout the context. To update the display, you can update the renderable:

import time

from rich.live import Live
from rich.table import Table

table = Table()
table.add_column("Row ID")
table.add_column("Description")
table.add_column("Level")

with Live(table, refresh_per_second=4):  # update 4 times a second to feel fluid
    for row in range(12):
        time.sleep(0.4)  # arbitrary delay
        # update the renderable internally
        table.add_row(f"{row}", f"description {row}", "[red]ERROR")

For more details, read the docs

Rich supports more features like Layouts, and interacting with the console protocol

Conclusion

Thank you for reading! Follow us on Twitter for more tech blogs. To learn more about Rich you can take a look at their wonderful documentation, on which this blog was based upon.

Until next time, Sourajyoti

 
Share this