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.
Get Your Code: Click here to download free sample code that shows you how to use Rich for more beautiful Python code and apps.
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:
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:
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:
The syntax highlighting makes it easy to see what types the structure contains.
Python’s standard library includes a package named pprint
that supports pretty-printing. While its formatting of complex data structures is a definite improvement over the built-in print()
, it doesn’t support colored text.
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":"john.jones@heroes.are.us"},
... {"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:
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:
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:
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.
Note: The print()
function that comes with Rich is a shortcut that supports some of the Console
features. In longer programs, you should prefer Console
and its .print()
method over rich.print()
as it’s more powerful.
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:
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:
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:
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:
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 rich_status.py
to investigate how this works:
dynamic_status.py
1import time
2from rich.console import Console
3
4def do_something_important():
5 time.sleep(5.0) # Simulates a long process
6
7console = Console()
8with console.status(
9 "Please wait - solving global problems...", spinner="earth"
10):
11 do_something_important()
12
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:
progress_indicator.py
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)
time.sleep(0.01)
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.
Note: You can use console markup in most strings, including the progress descriptions in the example. As a shorthand, you can use [/]
to close the last applied style.
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:
noble_gases.py
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:
table.add_row(
noble_gas["name"],
noble_gas["symbol"],
str(noble_gas["atomic_number"]),
str(noble_gas["atomic_mass"]),
noble_gas["properties"],
)
console.print(table)
A table can be a very convenient way of presenting this sort of data:
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:
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": "Crypto.com 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 live_table.py
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:
live_table.py
1import contextlib
2import json
3import time
4from pathlib import Path
5from rich.console import Console
6from rich.live import Live
7from rich.table import Table
8
9console = Console()
10
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
38
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
44
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 live_table.py
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.
Conclusion
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.
Get Your Code: Click here to download free sample code that shows you how to use Rich for more beautiful Python code and apps.
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