The Python Rich Package: Unleash the Power of Console Text

The Python Rich Package: Unleash the Power of Console Text

by Charles de Villiers Nov 27, 2023 intermediate front-end python

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Unleashing the Power of the Console With Rich

Python’s Rich package is a tool kit that helps you generate beautifully formatted and highlighted text in the console. More broadly, it allows you to build an attractive text-based user interface (TUI).

Why would you choose a TUI over a graphical user interface, or GUI? Sometimes a text display feels more appropriate. Why use a full-blown GUI for a simple application, when an elegant text interface will do? It can be refreshing to work with plain text. Text works in almost any hardware environment, even on an SSH terminal or a single-board computer display. And many applications don’t need the complexity of a full graphical windowing system.

In this tutorial, you’ll learn how Rich can help you:

  • Enhance the user interface of command-line tools
  • Improve the readability of console output
  • Create attractive dashboard displays for real-time tabular data
  • Generate well-formatted reports

Will McGugan, the author of Rich, has also developed the Textual package. Whereas Rich is a rich-text tool kit, Textual is a full application framework built on Rich. It provides application base classes, an event-driven architecture, and more.

There’s a lot you can do with Rich on its own, and its support for engaging, dynamic displays may well be sufficient for your app. By following this tutorial, you’ll experiment with many of the cool features of Rich, and you’ll finish up by using your skills to build a dynamically scrolling tabular display of crypto prices:

To fully understand Rich’s syntax for animations, you should have a good grasp of context managers. But if you’re a bit rusty, don’t worry! You’ll get a quick refresher in this tutorial.

Installing Rich

You can start using Rich very quickly. As always when starting a new project or investigation, it’s best to create a virtual environment first, to avoid polluting your system’s Python installation.

It’s quite possible to install Rich and use it with the built-in Python REPL, but for a better developer experience, you may want to include support for Jupyter notebooks. Here’s how you can install Rich so that it’ll work with either the REPL or Jupyter:

Windows PowerShell
PS> python -m venv venv
PS> venv\Scripts\activate
(venv) PS> python -m pip install rich[jupyter]
$ python -m venv venv
$ source venv/bin/activate
(venv) $ python -m pip install "rich[jupyter]"

Now that you’ve installed Rich in your new virtual environment, you can test it and also get a nice overview of its capabilities:

(venv) $ python -m rich

Running this command will make lots of magic happen. Your terminal will fill with color, and you’ll see several options for customizing your text-based user interface:

Apart from displaying colorful text in a variety of styles, this demo also illustrates a few more of Rich’s exciting features.

You can wrap and justify text. You can easily display any Unicode characters, as well as a wide choice of emojis. Rich renders Markdown and offers a syntax for creating elegantly formatted tables.

There’s much more that you can do with Rich, as you’ll discover throughout this tutorial. You can also try some of the command-line demos that Rich has thoughtfully provided for its subpackages, so you can get a flavor of what each one can do without writing any code.

Here are a few that you can try from your OS console. Execute them one by one to get a feel for the power of Rich:

(venv) $ python -m rich.table
(venv) $ python -m rich.progress
(venv) $ python -m rich.status

Most of these demos are very short, but if necessary, you can always interrupt them with Ctrl+C.

With the installation done, you’re ready to start exploring Rich.

Using Rich for Python Development

Rich can help to make your life as a developer a little easier. For instance, it has built-in support for formatting and syntax-highlighting Python code and data structures, and it has a very useful inspect() function that lets you examine deeply nested data.

You can make the most of Rich’s development support by using it with IPython or Jupyter. If you don’t know these tools, then it’s well worth giving them a try. Rich provides special support for both of them. Here, though, you’ll be exploring Rich with the built-in REPL.

Syntax Highlighting

The first Rich feature that you’ll explore is syntax highlighting. This helps to clarify the structure of programming statements and data. Syntax highlighting is built into Rich’s print() function. Open the Python REPL, define a simple data structure, and print it using Python’s built-in print() function:

>>> student = { "person": {"name": "John Jones", "age": 30, "subscriber": True}}
>>> print(student)

This does just what you’d expect:

Displaying a simple data structure with the built-in print()

That output has all the information that you wanted to display, but it doesn’t look very exciting. Wouldn’t it be nice if the different data types were color-coded to provide some visual variety and help the user keep the information straight? The Rich version does just that:

>>> from rich import print as rprint
>>> rprint(student)

Now the different data types are highlighted:

Displaying a simple data structure using Rich's print()

The syntax highlighting makes it easy to see what types the structure contains.

Since Rich’s print() is a drop-in replacement for Python’s built-in function, you could safely override the built-in print(). Here, though, to make comparisons easier, you’ve imported it under the alias rprint().

The real benefits of pretty-printing code appear when you’re dealing with more complex structures. Here’s a small example. Suppose you’re designing a data structure for your latest app, and you’ve hand-coded this dictionary:

>>> superhero = {"person": {"name": "John Jones", "age":30,
... "address":{"street": "123 Main St", "city": "Gotham",
... "state":"NY", "zip_code": "12345"},
... "superpowers":{"leaps_buildings":True,"factorizes_polynomials":False},
... "contacts":[{"type":"email","value":""},
... {"type":"phone","value":"555-123-4567"}],
... "hobbies": ["reading","hiking","coding","crimefighting"],
... "family":{"spouse": {"name":"Griselda Jones", "age":28},
... "children":[{"name":"Bellatrix", "age":5, "name":"Draco", "age":8}
... ]}}}

The Python REPL is fairly forgiving about input format. So long as your code is valid Python, as here, the REPL is happy to accept it.

You carry on coding. A little later, you decide to write the code that parses this data structure. To remind yourself of the details, you print it:

Plain REPL print() of superhero data structure

The data is all there, but the REPL has its own ideas about formatting. It’s certainly possible to trawl through this print() output and identify the nested dictionaries and lists. But it’s already a bit of a tiresome task, and a much larger structure could be really annoying. Now see what happens when you pretty-print it:

>>> rprint(superhero)

The code is neatly formatted and takes advantage of syntax highlighting:

Complex data structure with syntax highlighting

Not only does the layout make more sense, but the various data types are colored differently. You’ll probably agree that this makes it much easier to understand the structure.

Do you always want your data structures presented like this while you’re developing? As you probably know, when you simply type a variable name into the REPL and press Enter, it echoes that variable’s value to the console. By default, the format is the same as for print(). But wouldn’t it be nice to have that value pretty-printed by default? You can install Rich pretty-printing in the REPL:

>>> from rich import pretty
>>> pretty.install()

Now, simply by typing your variable’s name in the REPL, you’ll automatically get a pretty-printed and highlighted representation, just like the rprint() output above. You can even configure your REPL to always install Rich’s pretty-printing at startup. Having your data structures displayed in a pleasing and readable format can really make your coding experience more pleasant and productive!

Code Object Inspection

It’s great that your data structures are now prettily color-coded and formatted, but that’s not always enough in development. Sometimes you want to peek under the hood of a data structure and really get a snapshot of how it works. For that, you can use Rich’s inspect() function. See what it can do with your example data structure:

>>> from rich import inspect
>>> inspect(superhero, methods=True)

With inspect(), you can really examine the inner workings of an object. Since superhero is a dict, Rich shows you all the available dict constructors before displaying the pretty-printed data as before. Next, courtesy of the optional methods=True parameter, you also get a handy summary of the object’s methods with their short docstrings and parameter types:

Rich.inspect() of the superhero data structure

The inspect() function is quite powerful. You can get a comprehensive description of its parameters by typing inspect(inspect) as suggested in the output above.

The Python standard library has its own inspect module that also allows live inspection of code objects. It’s much more powerful, and also much more complex, than the Rich function that you’ve been using. You should check it out if you need heavy-duty code introspection. Python’s inspect module doesn’t offer syntax highlighting, however. For on-the-fly investigations, you may find the Rich function more convenient.

The Console Class

Rich has a Console class that encapsulates most of the package’s capabilities. A Console instance can format text, generate colorized log output, or pretty-print JSON, and it can also handle indentation, horizontal rules, widgets, panels, and tables, as well as interactive prompts and animations. You can capture Console output and export it as text, SVG, or HTML.

Console will interpret console markup to apply colors and attributes to text on the fly. All these features are available subject to your terminal’s capabilities, but most modern terminal emulators will handle everything just fine. Console markup consists of paired tags within square brackets:

>>> from rich.console import Console
>>> console = Console()
>>> console.print("[green underline]Green underline[/green underline] "
... "[blue italic]Blue italic[/blue italic]")

The specified styles are applied to the text within the tags:

Example of Rich Console Markup

You can find a complete list of the names, hex codes, and RGB values of the 255 standard text colors in the Rich appendix. You can also view them from the command line:

(venv) $ python -m rich.color

If your terminal supports true colors, then you can specify any of the sixteen million colors available by their RGB values. Along with a full range of text colors, console markup supports attributes like bold, blink, reverse, underline, and italic.

A combination of a color and attributes is called a Style, and you can put several styles together in a dictionary to create a Theme:

>>> from rich.console import Console
>>> from rich.theme import Theme

>>> custom_theme = Theme(
...     {"info": "bold cyan", "warning": "magenta", "danger": "bold red"}
... )
>>> console = Console(theme=custom_theme)

In custom_theme, you’ve defined complementary styles that you can apply to any Console instance:

Rich text with custom theme

Using a Console object with a Theme helps to keep your formatting consistent across the application.

Logging and Tracebacks

Creating good log statements is an important part of writing maintainable code. Logging helps you during development and testing by confirming that your code is following the expected paths. It’s invaluable when things go wrong in production code, as well-placed log statements can provide insight into obscure code misbehavior.

The Console class supports formatted logging and uses a syntax that’s very close to that of Python’s standard logging package. It can generate nicely formatted tracebacks of any uncaught exceptions in your code while using styles from a defined Theme.

Python’s built-in tracebacks and error messages get more informative and useful with each new release, but some extra eye appeal doesn’t hurt, and there’s no substitute for good log messages to give context for a crash. Continuing the previous example, you can produce some samples of logging output:

>>> from rich.traceback import install
>>> install(show_locals=True)

Log messages can also use the Theme that you defined in the previous code snippet:

Log output showing custom theme

With console.log(), you automatically add timestamps, source filenames, and source line numbers to your logging statements.

While things are running normally, you’ll just get the clean log output, as above. But if there’s a crash during development, then you’ll want to log as much information as possible about the cause. Try manually throwing a RuntimeError, and see the difference:

Rich Console Log With Stacktrace and Local Variables

When an exception happens, the traceback can optionally display all your local variables as well as the stack trace of the error. Your session may contain more local variables than this, so the output may look different.

Tools like these can really help make your development experience more pleasant and productive. Rich’s colorful and nicely presented error messages are much more readable than Python’s default tracebacks. You may actually look forward to getting an exception!

Keeping Your User Engaged Through Animation

There’s much more to Rich than just developer tools. Its prime purpose is to help you create an interesting and attractive interface for the user. Animations can be a powerful asset in this aim. Rich’s animation tools are designed to make use of context managers. Here, you’ll do a quick review of what context managers are and how they work. If you’d like a more in-depth discussion, you can visit Context Managers and Python’s with Statement.

Understanding Context Managers

A context manager is a mechanism for keeping track of resources. You can use it in any situation where a resource such as a file, a socket, or a thread must be allocated to a task and then later returned to the system. A context manager in Python is invoked with the with keyword, and it’s active within the subsequent indented block. When the code execution leaves the block, the teardown actions are invoked automatically.

In the case of Rich animations, the resources are the timers and variables for keeping track of the changes in the display. The Rich library provides context managers to handle these resources. You, the programmer, can largely ignore the complexities and trust the context manager to do what’s right.

Displaying Dynamic Status With Animations

Rich has a Status class that you can use to display the status of your program. The recommended way to use it is as a context manager. Create a file named to investigate how this works:

 1import time
 2from rich.console import Console
 4def do_something_important():
 5    time.sleep(5.0)  # Simulates a long process
 7console = Console()
 8with console.status(
 9    "Please wait - solving global problems...", spinner="earth"
11    do_something_important()
13console.print("All fixed! :sunglasses:")

The context manager scope starts at line 8. You call console.status() with two parameters: the message to display and the animated spinner that appears along with the message while the context block is active.

Your call to do_something_important() in line 11 happens within the context manager’s scope, so the message and the spinner remain visible until that function returns five seconds later. Finally, in line 13, you announce success. The status() animation disappears, making way for a cheery announcement and an emoji:

So, the particular spinner parameter in this example rendered an image of the rotating planet. But how can you find out what other spinners you can use? You can see all the numerous spinner animations available by using another handy module-level demo:

(venv) $ python -m rich.spinner

You’ll see that you have a wide choice of geometric animations, plus some more pictorial ones, like "clock", "smiley", and "weather".

Similarly, the :sunglasses: syntax in line 11 shows how you can incorporate static emojis into inline text using colon-delimited names. You can insert emojis wherever Rich renders text, such as in a log() statement, a prompt, or a table. And of course, Rich has thousands more emojis that you can use in this way. As with the spinner images, you can display the full catalog of emoji names and images using yet another command-line demo:

(venv) $ python -m rich.emoji

There are far too many emojis available to fit on a single screen.

If you prefer an online reference, then you can also find spinners and emojis cataloged in the Rich documentation.

Animating Activities With Progress Bars

Animated spinners are well and good if the wait is short, but for a lengthier process, you’ll want to give the user some indication of how long they can expect to wait. For this case, Rich’s Progress class has you covered. Instances of this class keep track of one or more asynchronous tasks while displaying their progress through an animated bar, a percent-completed display, and an estimated time to completion. Here’s how you can code that up:

import time
from rich.progress import Progress

with Progress() as progress:
    task1 = progress.add_task("[red]Fribbulating...[/]", total=1000)
    task2 = progress.add_task("[green]Wobbulizing...[/]", total=1000)
    task3 = progress.add_task("[cyan]Fandangling...[/]", total=1000)
    while not progress.finished:
        progress.update(task1, advance=0.5)
        progress.update(task2, advance=0.3)
        progress.update(task3, advance=0.9)

The Progress class animates and highlights the display as your tasks progress. Just for illustrative purposes, your tasks all advance at different speeds:

In a real application, you could call the .update() method for a task such as a file download, with the advance parameter calculated from the number of bytes downloaded.

As with most Rich classes, there are plenty of ways to customize the details of what Progress displays. You can check out the details in the Rich documentation

Bringing Tables to Life

If you’re working with a lot of data, then often a table is the most compact way to present it. But plain data tables can be a little dry. In this section, you’ll see how you can use Rich’s table-building tools along with formatting and colorization to build a table that really grabs the user’s attention.

Static tables have their uses, but now you’ll add some real pizzazz. You’ll use Rich animation to convert your table into a simulated real-time display. A scrolling, updating table like this could be the main feature of your app, or it could be just one part of a whole data dashboard.

Building a Static Table

Rich has a Table class that lets you build nice tabular data displays. Here’s an example that shows how you can set formats and colors on a per-column basis:

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

console = Console()

table = Table(title="Noble Gases")
table.add_column("Name", style="cyan", justify="center")
table.add_column("Symbol", style="magenta", justify="center")
table.add_column("Atomic Number", style="yellow", justify="right")
table.add_column("Atomic Mass", style="green", justify="right")
table.add_column("Main Properties", style="blue", justify="center")

noble_gases = [
    {"name": "Helium", "symbol": "He", "atomic_number": 2,
     "atomic_mass": 4.0026, "properties": "Inert gas"},
    {"name": "Neon", "symbol": "Ne", "atomic_number": 10,
     "atomic_mass": 20.1797, "properties": "Inert gas"},
    {"name": "Argon", "symbol": "Ar", "atomic_number": 18,
     "atomic_mass": 39.948, "properties": "Inert gas"},
    {"name": "Krypton", "symbol": "Kr", "atomic_number": 36,
     "atomic_mass": 83.798, "properties": "Inert gas"},
    {"name": "Xenon", "symbol": "Xe", "atomic_number": 54,
     "atomic_mass": 131.293, "properties": "Inert gas"},
    {"name": "Radon", "symbol": "Rn", "atomic_number": 86,
     "atomic_mass": 222.0, "properties": "Radioactive gas"},
    {"name": "Oganesson", "symbol": "Og", "atomic_number": 118,
     "atomic_mass": "(294)", "properties": "Synthetic radioactive gas"},

for noble_gas in noble_gases:


A table can be a very convenient way of presenting this sort of data:

Rich Static Table of Noble Gasses

The Table API gives you an intuitive way of building a nice-looking tabular display.

Animating a Scrolling Display

A static table is fine for displaying static data, but what if you want to display dynamic data in real time? Rich has a class named Live that helps you do this. Live is a context manager that takes control of the console formatting, allowing you to update whatever fields you like without disturbing the rest of the layout. For your Live demo, you’ll make use of some real cryptocurrency data, captured from a free crypto API.

To keep the demo focused on the display aspects, you’ll use canned data to simulate real-time updates. There are one hundred entries that you’ll present in a table only twenty rows deep. To do this, you’ll scroll the data through the table in an endless loop, simulating the continuous arrival of new data.

In a real application, you might be getting real-time updates from a crypto API, which you’d then add to the bottom of your table while allowing the older data to scroll off the top. The overall visual effect would be very similar to your demo.

Accessing the Crypto Data

Since the crypto data is a little bulky, you’ll put it in a separate JSON-file:

JSON crypto_data.json
        "symbol": "BTC",
        "name": "Bitcoin",
        "price_usd": "31252.03",
        "percent_change_7d": "3.79",
        "volume24": 20953193815.93972
        "symbol": "ETH",
        "name": "Ethereum",
        "price_usd": "1996.94",
        "percent_change_7d": "7.72",
        "volume24": 43776094155.557755
        "symbol": "USDT",
        "name": "Tether",
        "price_usd": "1.00",
        "percent_change_7d": "0.07",
        "volume24": 55137371360.06878
        "symbol": "BNB",
        "name": "Binance Coin",
        "price_usd": "254.17",
        "percent_change_7d": "8.54",
        "volume24": 788139087.131846
        "symbol": "USDC",
        "name": "USD Coin",
        "price_usd": "0.999758",
        "percent_change_7d": "-0.03",
        "volume24": 34890207329.59203
        "symbol": "XRP",
        "name": "XRP",
        "price_usd": "0.776092",
        "percent_change_7d": "65.51",
        "volume24": 13289725792.18076
        "symbol": "BUSD",
        "name": "Binance USD",
        "price_usd": "1.00",
        "percent_change_7d": "-0.01",
        "volume24": 2982268496.878559
        "symbol": "ADA",
        "name": "Cardano",
        "price_usd": "0.348179",
        "percent_change_7d": "23.40",
        "volume24": 1317758234.4725194
        "symbol": "SOL",
        "name": "Solana",
        "price_usd": "27.96",
        "percent_change_7d": "37.47",
        "volume24": 2608578104.9363656
        "symbol": "DOGE",
        "name": "Dogecoin",
        "price_usd": "0.070784",
        "percent_change_7d": "7.82",
        "volume24": 776208544.8693453
        "symbol": "STETH",
        "name": "Staked Ether",
        "price_usd": "1984.92",
        "percent_change_7d": "8.38",
        "volume24": 970870.4192054314
        "symbol": "TRX",
        "name": "TRON",
        "price_usd": "0.082126",
        "percent_change_7d": "5.43",
        "volume24": 294442942.6567772
        "symbol": "LTC",
        "name": "Litecoin",
        "price_usd": "100.92",
        "percent_change_7d": "3.73",
        "volume24": 2010476368.057925
        "symbol": "DOT",
        "name": "Polkadot",
        "price_usd": "5.63",
        "percent_change_7d": "10.75",
        "volume24": 282938041.0123782
        "symbol": "WBTC",
        "name": "Wrapped Bitcoin",
        "price_usd": "31201.84",
        "percent_change_7d": "3.62",
        "volume24": 36161715.175137974
        "symbol": "BCH",
        "name": "Bitcoin Cash",
        "price_usd": "272.20",
        "percent_change_7d": "-4.50",
        "volume24": 2129638945.9266052
        "symbol": "AVAX",
        "name": "Avalanche",
        "price_usd": "15.24",
        "percent_change_7d": "20.96",
        "volume24": 500476432.5450047
        "symbol": "UNI",
        "name": "Uniswap",
        "price_usd": "5.96",
        "percent_change_7d": "10.67",
        "volume24": 122940409.6610347
        "symbol": "SHIB",
        "name": "Shiba Inu",
        "price_usd": "0.000008",
        "percent_change_7d": "10.67",
        "volume24": 204280913.4426844
        "symbol": "XLM",
        "name": "Stellar",
        "price_usd": "0.142132",
        "percent_change_7d": "46.19",
        "volume24": 1263932212.2337687
        "symbol": "LEO",
        "name": "UNUS SED LEO",
        "price_usd": "3.97",
        "percent_change_7d": "8.41",
        "volume24": 2339911.236200216
        "symbol": "LINK",
        "name": "ChainLink",
        "price_usd": "7.10",
        "percent_change_7d": "15.50",
        "volume24": 484358760.1126672
        "symbol": "XMR",
        "name": "Monero",
        "price_usd": "164.65",
        "percent_change_7d": "-1.88",
        "volume24": 2038036008.0313425
        "symbol": "ATOM",
        "name": "Cosmos",
        "price_usd": "10.01",
        "percent_change_7d": "7.68",
        "volume24": 222431999.6788462
        "symbol": "ETC",
        "name": "Ethereum Classic",
        "price_usd": "19.89",
        "percent_change_7d": "4.32",
        "volume24": 382469987.24052006
        "symbol": "OKB",
        "name": "OKB",
        "price_usd": "44.00",
        "percent_change_7d": "3.71",
        "volume24": 5182537.475964661
        "symbol": "LDO",
        "name": "Lido DAO",
        "price_usd": "2.43",
        "percent_change_7d": "25.87",
        "volume24": 200042470.0922766
        "symbol": "TON",
        "name": "The Open Network",
        "price_usd": "1.36",
        "percent_change_7d": "1.87",
        "volume24": 4290911.169640025
        "symbol": "MATIC",
        "name": "Matic Network",
        "price_usd": "0.841325",
        "percent_change_7d": "25.56",
        "volume24": 715840267.2564398
        "symbol": "FIL",
        "name": "Filecoin",
        "price_usd": "4.61",
        "percent_change_7d": "5.08",
        "volume24": 276195488.1582698
        "symbol": "ARB",
        "name": "Arbitrum",
        "price_usd": "1.25",
        "percent_change_7d": "14.98",
        "volume24": 207234601.30264035
        "symbol": "VET",
        "name": "VeChain",
        "price_usd": "0.020024",
        "percent_change_7d": "7.60",
        "volume24": 70424000.50503708
        "symbol": "NEAR",
        "name": "NEAR Protocol",
        "price_usd": "1.55",
        "percent_change_7d": "16.68",
        "volume24": 186510632.14473444
        "symbol": "QNT",
        "name": "Quant",
        "price_usd": "105.03",
        "percent_change_7d": "0.81",
        "volume24": 32760026.63178301
        "symbol": "AAVE",
        "name": "Aave",
        "price_usd": "83.40",
        "percent_change_7d": "16.36",
        "volume24": 435033534.55578315
        "symbol": "GRT",
        "name": "The Graph",
        "price_usd": "0.124238",
        "percent_change_7d": "3.75",
        "volume24": 50745768.31360009
        "symbol": "FRAX",
        "name": "Frax",
        "price_usd": "0.999128",
        "percent_change_7d": "0.54",
        "volume24": 10112664.516622534
        "symbol": "TUSD",
        "name": "TrueUSD",
        "price_usd": "0.999479",
        "percent_change_7d": "-0.05",
        "volume24": 3269190962.729144
        "symbol": "STX",
        "name": "Stacks",
        "price_usd": "0.680968",
        "percent_change_7d": "4.52",
        "volume24": 72956976.43657969
        "symbol": "MKR",
        "name": "Maker",
        "price_usd": "913.72",
        "percent_change_7d": "-9.45",
        "volume24": 325122162.91072315
        "symbol": "EOS",
        "name": "EOS",
        "price_usd": "0.804691",
        "percent_change_7d": "12.59",
        "volume24": 218036299.95140857
        "symbol": "USDP",
        "name": "Pax Dollar",
        "price_usd": "0.999809",
        "percent_change_7d": "-0.08",
        "volume24": 1627155.176109821
        "symbol": "ALGO",
        "name": "Algorand",
        "price_usd": "0.118303",
        "percent_change_7d": "3.50",
        "volume24": 175432880.12364262
        "symbol": "XTZ",
        "name": "Tezos",
        "price_usd": "0.902678",
        "percent_change_7d": "14.45",
        "volume24": 17862226.065150194
        "symbol": "FTM",
        "name": "Fantom",
        "price_usd": "0.299238",
        "percent_change_7d": "11.48",
        "volume24": 187495267.91607383
        "symbol": "THETA",
        "name": "Theta Token",
        "price_usd": "0.827153",
        "percent_change_7d": "14.36",
        "volume24": 39602951.52502267
        "symbol": "MANA",
        "name": "Decentraland",
        "price_usd": "0.433536",
        "percent_change_7d": "14.95",
        "volume24": 125883681.50783731
        "symbol": "BCHSV",
        "name": "Bitcoin SV",
        "price_usd": "39.35",
        "percent_change_7d": "-10.48",
        "volume24": 65176121.64507406
        "symbol": "USDD",
        "name": "USDD",
        "price_usd": "1.00",
        "percent_change_7d": "0.18",
        "volume24": 10943706.717842644
        "symbol": "SAND",
        "name": "The Sandbox",
        "price_usd": "0.467742",
        "percent_change_7d": "12.10",
        "volume24": 200640950.97449806
        "symbol": "PEPE",
        "name": "Pepe",
        "price_usd": "0.000002",
        "percent_change_7d": "11.25",
        "volume24": 406548396.3096544
        "symbol": "APE",
        "name": "APEcoin",
        "price_usd": "2.21",
        "percent_change_7d": "14.72",
        "volume24": 296669905.1748844
        "symbol": "NEO",
        "name": "Neo",
        "price_usd": "9.59",
        "percent_change_7d": "5.63",
        "volume24": 87953825.32299156
        "symbol": "FLOW",
        "name": "Flow",
        "price_usd": "0.652914",
        "percent_change_7d": "4.75",
        "volume24": 88816184.52336611
        "symbol": "AXS",
        "name": "Axie Infinity",
        "price_usd": "6.63",
        "percent_change_7d": "12.65",
        "volume24": 173301949.07415962
        "symbol": "CRV",
        "name": "Curve DAO Token",
        "price_usd": "0.863588",
        "percent_change_7d": "17.72",
        "volume24": 34442558.73801766
        "symbol": "INJ",
        "name": "Injective Protocol",
        "price_usd": "9.64",
        "percent_change_7d": "21.61",
        "volume24": 214944721.17498806
        "symbol": "PHB",
        "name": "Red Pulse Phoenix",
        "price_usd": "0.739450",
        "percent_change_7d": "11.41",
        "volume24": 2127239.3825587
        "symbol": "XEC",
        "name": "eCash",
        "price_usd": "0.000031",
        "percent_change_7d": "-19.12",
        "volume24": 17398291.14119232
        "symbol": "CRO",
        "name": " Chain",
        "price_usd": "0.060308",
        "percent_change_7d": "6.23",
        "volume24": 10125581.086761687
        "symbol": "CHZ",
        "name": "Chiliz",
        "price_usd": "0.083790",
        "percent_change_7d": "10.56",
        "volume24": 46253360.29975289
        "symbol": "KCS",
        "name": "KuCoin Shares",
        "price_usd": "6.24",
        "percent_change_7d": "-1.52",
        "volume24": 3496576.426964752
        "symbol": "KLAY",
        "name": "Klaytn",
        "price_usd": "0.177168",
        "percent_change_7d": "5.05",
        "volume24": 19656059.820330244
        "symbol": "RNDR",
        "name": "Render Token",
        "price_usd": "2.14",
        "percent_change_7d": "10.64",
        "volume24": 92808359.23988137
        "symbol": "FTT",
        "name": "FTX Token",
        "price_usd": "1.64",
        "percent_change_7d": "10.01",
        "volume24": 53872321.04347122
        "symbol": "MIOTA",
        "name": "IOTA",
        "price_usd": "0.192000",
        "percent_change_7d": "4.59",
        "volume24": 8649448.187102558
        "symbol": "ZEC",
        "name": "Zcash",
        "price_usd": "32.34",
        "percent_change_7d": "3.78",
        "volume24": 234082644.50086042
        "symbol": "PAXG",
        "name": "PAX Gold",
        "price_usd": "1935.85",
        "percent_change_7d": "2.19",
        "volume24": 812583422.7762803
        "symbol": "LUNC",
        "name": "Terra Classic",
        "price_usd": "0.000088",
        "percent_change_7d": "5.29",
        "volume24": 29817037.116504386
        "symbol": "HBAR",
        "name": "Hedera Hashgraph",
        "price_usd": "0.053269",
        "percent_change_7d": "14.34",
        "volume24": 41598576.611335196
        "symbol": "TACO",
        "name": "Tacos",
        "price_usd": "78.94",
        "percent_change_7d": "18.34",
        "volume24": 6717589.401493353
        "symbol": "EGLD",
        "name": "Elrond eGold",
        "price_usd": "37.59",
        "percent_change_7d": "11.35",
        "volume24": 22131636.4517117
        "symbol": "GMX",
        "name": "GMX",
        "price_usd": "60.02",
        "percent_change_7d": "9.75",
        "volume24": 30717637.271570735
        "symbol": "COMP",
        "name": "Compound",
        "price_usd": "68.31",
        "percent_change_7d": "17.87",
        "volume24": 123188541.96973641
        "symbol": "TKX",
        "name": "Tokenize Xchange",
        "price_usd": "6.19",
        "percent_change_7d": "7.04",
        "volume24": 11293471.330545316
        "symbol": "XAUT",
        "name": "Tether Gold",
        "price_usd": "1959.50",
        "percent_change_7d": "2.51",
        "volume24": 6054015.365067729
        "symbol": "KAS",
        "name": "Kaspa",
        "price_usd": "0.028860",
        "percent_change_7d": "33.85",
        "volume24": 12770039.919820618
        "symbol": "HT",
        "name": "Huobi Token",
        "price_usd": "2.78",
        "percent_change_7d": "3.13",
        "volume24": 12653864.943357613
        "symbol": "CSPR",
        "name": "Casper",
        "price_usd": "0.038206",
        "percent_change_7d": "1.91",
        "volume24": 5576587.639663127
        "symbol": "DASH",
        "name": "Dash",
        "price_usd": "35.85",
        "percent_change_7d": "5.28",
        "volume24": 71757437.09998573
        "symbol": "XDCE",
        "name": "XinFin Network",
        "price_usd": "0.032674",
        "percent_change_7d": "1.71",
        "volume24": 1776317.812194609
        "symbol": "KAVA",
        "name": "Kava",
        "price_usd": "0.967047",
        "percent_change_7d": "4.15",
        "volume24": 73097054.46498355
        "symbol": "RPL",
        "name": "Rocket Pool",
        "price_usd": "37.84",
        "percent_change_7d": "-1.74",
        "volume24": 1054669.705829167
        "symbol": "SUI",
        "name": "Sui",
        "price_usd": "0.725466",
        "percent_change_7d": "11.08",
        "volume24": 92574462.3637904
        "symbol": "NEXO",
        "name": "Nexo",
        "price_usd": "0.658019",
        "percent_change_7d": "4.53",
        "volume24": 5663554.267356189
        "symbol": "FLEX",
        "name": "FLEX Coin",
        "price_usd": "3.66",
        "percent_change_7d": "0.04",
        "volume24": 320013.9077801702
        "symbol": "ZIL",
        "name": "Zilliqa",
        "price_usd": "0.022452",
        "percent_change_7d": "8.17",
        "volume24": 40085286.38067658
        "symbol": "TWT",
        "name": "Trust Wallet Token",
        "price_usd": "0.854440",
        "percent_change_7d": "-0.84",
        "volume24": 16348028.542735066
        "symbol": "RUNE",
        "name": "THORChain",
        "price_usd": "1.07",
        "percent_change_7d": "5.55",
        "volume24": 12001247.683048533
        "symbol": "FEI",
        "name": "Fei USD",
        "price_usd": "0.815600",
        "percent_change_7d": "-11.06",
        "volume24": 940166.3214333741
        "symbol": "SNX",
        "name": "Synthetix Network Token",
        "price_usd": "2.95",
        "percent_change_7d": "41.08",
        "volume24": 311179071.90065503
        "symbol": "DYDX",
        "name": "dYdX",
        "price_usd": "2.14",
        "percent_change_7d": "15.76",
        "volume24": 74017943.36035483
        "symbol": "AGIX",
        "name": "SingularityNET",
        "price_usd": "0.274559",
        "percent_change_7d": "18.35",
        "volume24": 195860297.72295615
        "symbol": "LRC",
        "name": "Loopring",
        "price_usd": "0.243839",
        "percent_change_7d": "7.30",
        "volume24": 37783839.42544796
        "symbol": "ENJ",
        "name": "Enjin Coin",
        "price_usd": "0.319967",
        "percent_change_7d": "8.14",
        "volume24": 22514225.851048034
        "symbol": "BAT",
        "name": "Basic Attention Token",
        "price_usd": "0.213415",
        "percent_change_7d": "11.79",
        "volume24": 19548090.541221704
        "symbol": "GNO",
        "name": "Gnosis",
        "price_usd": "121.48",
        "percent_change_7d": "7.51",
        "volume24": 2013836.000557435
        "symbol": "CVX",
        "name": "Convex Finance",
        "price_usd": "4.22",
        "percent_change_7d": "6.44",
        "volume24": 2635140.659348456
        "symbol": "OP",
        "name": "Optimism",
        "price_usd": "1.45",
        "percent_change_7d": "20.45",
        "volume24": 272662107.71766526
        "symbol": "QTUM",
        "name": "Qtum",
        "price_usd": "2.91",
        "percent_change_7d": "5.10",
        "volume24": 41268418.71165733

This JSON file contains a single large data structure: a list of dictionaries, each containing the named fields of interest. The real API reports more than two thousand symbols, but these one hundred will be enough for your demo.

Coding the Live Table

Next, you’ll write the module containing the code to display the dynamic table. This code will read data from crypto_data.json.

Your new module contains a single main function, make_table(coin_list). This function uses Rich’s Table class to generate a formatted table containing a section of your data. Then in the main module code, you’ll use the Live object’s .update() method to wrap a call to make_table() whenever your code updates the table data:

 1import contextlib
 2import json
 3import time
 4from pathlib import Path
 5from rich.console import Console
 6from import Live
 7from rich.table import Table
 9console = Console()
11def make_table(coin_list):
12    """Generate a Rich table from a list of coins"""
13    table = Table(
14        title=f"Crypto Data - {time.asctime()}",
15        style="black on grey66",
16        header_style="white on dark_blue",
17    )
18    table.add_column("Symbol")
19    table.add_column("Name", width=30)
20    table.add_column("Price (USD)", justify="right")
21    table.add_column("Volume (24h)", justify="right", width=16)
22    table.add_column("Percent Change (7d)", justify="right", width=8)
23    for coin in coin_list:
24        symbol, name, price, volume, pct_change = (
25            coin["symbol"],
26            coin["name"],
27            coin["price_usd"],
28            f"{coin['volume24']:.2f}",
29            float(coin["percent_change_7d"]),
30        )
31        pct_change_str = f"{pct_change:2.1f}%"
32        if pct_change > 5.0:
33            pct_change_str = f"[white on dark_green]{pct_change_str:>8}[/]"
34        elif pct_change < -5.0:
35            pct_change_str = f"[white on red]{pct_change_str:>8}[/]"
36        table.add_row(symbol, name, price, volume, pct_change_str)
37    return table
39# Load the coins data
40raw_data = json.loads(Path("crypto_data.json").read_text(encoding="utf-8"))
41num_coins = len(raw_data)
42coins = raw_data + raw_data
43num_lines = 20
45with Live(make_table(coins[:num_lines]), screen=True) as live:
46    index = 0
47    with contextlib.suppress(KeyboardInterrupt):
48        while True:
49            live.update(make_table(coins[index : index + num_lines]))
50            time.sleep(0.5)
51            index = (index + 1) % num_coins

The make_table() function starting at line 11 is similar to the code that you wrote previously for the static table of noble gases. There’s just one embellishment. Lines 31 to 35 provide different formatting for the pct_change field depending on whether it’s greater than 5 percent, less than -5 percent, or between these two values.

The num_lines variable in line 43 determines the number of lines in the displayed table. The animation magic happens when you invoke the Live context manager starting at line 45.

The optional screen=True parameter enables a nifty feature of Live. The original text display is saved, and the Live text appears on an alternate screen. The effect of this is that your program will seamlessly restore the original display when the function returns and exits the Live context.

The first parameter passed to Live is the table created by make_table(). Your program will call the same function each time it updates the display. On its first call in line 45, make_table() receives the first num_lines rows of coin data. In the subsequent calls wrapped in live.update() in line 49, the data progressively scrolls, using the index value as the starting point.

To simulate streaming data, you slice your static data. In line 42, you repeat the data twice in order to avoid complicated logic at the end of your dataset. You use the modulo operator (%) to restart index at 0 when you’ve shown all available data in the table.

Notice that the live-update code occurs within an infinite loop. When you’re tired of admiring your scrolling table, you can interrupt the code by pressing Ctrl+C. That interruption is caught by the suppress() context manager, which exits the loop and the Live context manager, stops the animation, and returns cleanly to your previous display.

You can invoke the table demo from the OS console:

(venv) $ python

This will display a scrolling table of twenty lines:

If you’d like to see a different table height, then you can modify num_lines in the code, or even make it a parameter for your script. You can probably think of plenty of ways to improve this table and make it suit your use case. But regardless of what you do to tweak your table, you have an attractive animation that should delight your user.


Congratulations on completing this whirlwind introduction to Python’s Rich library! You’ve built an animated, highlighted table display that could be the main component in a real-time data dashboard. Along the way, you’ve learned about using Rich in your development tool kit, and you’ve styled attractive end-user displays and lively status and progress widgets.

In this tutorial, you’ve learned how to:

  • Use Rich for stylish, helpful detail during coding, debugging and logging
  • Generate entertaining and informative animations to keep your user engaged during lengthy processing
  • Create a scrolling dashboard display that you could use to monitor a remote process

Now you’re equipped to start using Rich to enhance your development experience or to create an engaging TUI for your command-line application!

Do you see a role for Rich in your next project? Have you found more ways to exploit its power to create engaging user experiences? Leave a note in the comments below to share your insights!

Next Steps

There’s plenty left to explore about Rich. You’ve only scratched the surface here. There’s much more information in the Rich documentation. A nice starter project might be to build a Wordle clone using Rich.

Rich gives you a lot of control over layout, formatting, and paging, and it supports all types of terminals. It also supports some limited forms of interactive input, though for a fully interactive TUI experience, you’ll want to investigate Textual to see what that package has to offer.

If you’d like to hear more from Will McGugan, the developer of Rich, then you can check out his Python community interview or podcast episode here at Real Python. He also has a blog where he talks about his aims for Rich and Textual.

Watch Now This tutorial has a related video course created by the Real Python team. Watch it together with the written tutorial to deepen your understanding: Unleashing the Power of the Console With Rich

🐍 Python Tricks 💌

Get a short & sweet Python Trick delivered to your inbox every couple of days. No spam ever. Unsubscribe any time. Curated by the Real Python team.

Python Tricks Dictionary Merge

About Charles de Villiers

Charles teaches Physics and Math. When he isn't teaching or coding, he spends way too much time playing online chess.

» More about Charles

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

Master Real-World Python Skills With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

Master Real-World Python Skills
With Unlimited Access to Real Python

Locked learning resources

Join us and get access to thousands of tutorials, hands-on video courses, and a community of expert Pythonistas:

Level Up Your Python Skills »

What Do You Think?

Rate this article:

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.

Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Keep Learning