146. Rich (App V0.7)
RAM Manager v0.7 โ beautiful terminal output with Rich
146. Rich (App V0.7)
A wall of print() output is hard to read. Rich makes terminals beautiful.
๐ค Socratic question: Why do some CLI tools look amazing (with colors, tables, progress bars) while others just spew plain text? Itโs not magic โ itโs one library. Meet Rich.
๐๏ธ Origin story: Will McGugan built Rich as a weekend side project in 2020 while working a day job. Within a year it became one of the most starred Python repos on GitHub. He then quit his job to build Textual (a full TUI framework) full-time. This is the power of building in public and open source. One weekend project โ a company.
๐บ YouTube search: โWill McGugan Rich Python terminal talk PyConโ โ watch the creator explain why he built it.
from rich.console import Console
from rich.table import Table
from rich import box
console = Console()
table = Table(box=box.ROUNDED, title="RAM: 87.3%")
table.add_column('Process', style='cyan')
table.add_column('PID', justify='right')
table.add_column('RSS MB', justify='right')
for p in snapshot['processes']:
rss = p['rss_mb']
color = 'red' if rss > 500 else ('yellow' if rss > 100 else 'green')
table.add_row(p['name'], str(p['pid']), f'[{color}]{rss:.0f}[/]')
console.print(table)Output: โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ โ RAM: 87.3% โ โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโฌโโโโโโโโโโโค โ Process โ PID โ RSS MB โ โโโโโโโโโโโโโโโโโโโผโโโโโโโโโผโโโโโโโโโโโค โ chrome โ 812 โ 800 โ โ python โ 421 โ 150 โ โฐโโโโโโโโโโโโโโโโโโดโโโโโโโโโดโโโโโโโโโโโฏ
Two things catch him out:
- Closing tag is
[/]not[/red]โ Rich markup uses[/]to close any open tag Table.add_column('title')โ the argument is the column label, not a Column objectRich.Live()goes outside the update loop โ otherwise you create a new Live context every iteration and get flickering
๐ก Real-world: The Python packaging tools
pip,poetry, anduvall use Rich for their beautiful output. When you seepip installwith that nice progress bar โ thatโs Rich. Youโre using the same library as the tools you use every day.
๐ก Fun fact: Richโs markup syntax was inspired by BBCode (Bulletin Board Code), a tag language used in forums since the 1990s. Rich detects whether the terminal supports color (checking TERM, NO_COLOR, etc.) and automatically disables markup in environments that donโt support ANSI escape codes โ so it wonโt pollute log files or CI output with garbage characters.
โ ๏ธ Watch out: Rich uses [square brackets] for markup, NOT HTML <angle brackets>. Writing <red>text</red> prints the literal angle brackets โ it does not color anything. The correct syntax is [red]text[/]. Also, [/] closes any open tag โ you never need [/red] specifically.
๐ค Think about it: The solution opens Live() once outside the loop and calls live.update() each iteration. What visual difference would you see if you put with Live() as live: inside the loop? Why does Live need to know about the terminal size before the loop starts, and why does creating it fresh each iteration prevent that?
๐จ Aryan upgrades the RAM managerโs output from plain print() to a Rich table with color-coded process memory. He uses HTML tags instead of Rich markup, puts add_column() calls in the wrong order, and creates a new Live() inside the loop instead of outside it.
Learning objectives
- Use [color]โฆ[/] Rich markup (not HTML tags)
- Add columns with positional title argument
- Color-code by threshold (red > 500, yellow > 100, green otherwise)
- Keep Live() context outside the loop
Key concepts
- [red]text[/] โ Rich markup for colored output
- Table.add_column(โTitleโ) โ positional title
- Live(refresh_per_second=N) โ in-place terminal update
- live.update(renderable) โ replace content without scrolling
Try it
Concept detail
App v0.7 โ Rich terminal dashboard
โญโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฎ
โ Top Processes โ
โโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโค
โ Process โ PID โ RSS MB โ
โโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโค
โ chrome โ 812 โ 800 โ
โ python โ 421 โ 150 โ
โ vim โ 111 โ 20 โ
โฐโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโฏ# Rich markup โ square brackets, not HTML
f'[red]{rss:.0f}[/]' # red text, [/] closes all
f'[bold green]OK[/]' # bold green
console.print('[yellow]Warning![/]')
# Live dashboard pattern
with Live(refresh_per_second=2) as live:
while True:
live.update(make_table(take_snapshot()))
time.sleep(1)Solution
import time
import psutil
import os
import requests
from datetime import datetime
from dotenv import load_dotenv
from rich.console import Console
from rich.table import Table
from rich.live import Live
from rich import box
load_dotenv()
console = Console()
def take_snapshot(n: int = 10) -> dict:
mem = psutil.virtual_memory()
processes = []
for proc in psutil.process_iter(['pid', 'name', 'memory_info']):
try:
rss_mb = proc.info['memory_info'].rss / 1e6
processes.append({'name': proc.info['name'], 'pid': proc.info['pid'], 'rss_mb': rss_mb})
except (psutil.NoSuchProcess, psutil.AccessDenied):
pass
return {
'percent': mem.percent,
'used_gb': mem.used / 1e9,
'total_gb': mem.total / 1e9,
'processes': sorted(processes, key=lambda p: p['rss_mb'], reverse=True)[:n],
'timestamp': datetime.now().isoformat(),
}
def make_process_table(snapshot: dict) -> Table:
table = Table(box=box.ROUNDED, title='Top Processes')
table.add_column('Process', style='cyan')
table.add_column('PID', justify='right')
table.add_column('RSS MB', justify='right')
for p in snapshot['processes'][:10]:
rss = p['rss_mb']
color = 'red' if rss > 500 else ('yellow' if rss > 100 else 'green')
table.add_row(
p['name'],
str(p['pid']),
f'[{color}]{rss:.0f}[/]',
)
return table
def print_header(snapshot: dict) -> None:
pct = snapshot['percent']
color = 'red' if pct > 80 else ('yellow' if pct > 60 else 'green')
used = snapshot['used_gb']
total = snapshot['total_gb']
console.print(f'[{color}]RAM: {pct:.1f}% ({used:.1f}/{total:.1f} GB)[/]')
def run_live_dashboard(duration: int = 10):
# Live() context opened ONCE outside the loop
with Live(console=console, refresh_per_second=2) as live:
start = time.time()
while time.time() - start < duration:
snapshot = take_snapshot()
live.update(make_process_table(snapshot))
time.sleep(1)
def main():
snapshot = take_snapshot()
print_header(snapshot)
console.print(make_process_table(snapshot))
if __name__ == '__main__':
main()Tests
from io import StringIO
from rich.console import Console
from rich.table import Table
SAMPLE_SNAPSHOT = {
'percent': 72.5, 'used_gb': 11.6, 'total_gb': 16.0,
'timestamp': '2024-01-01T10:00:00',
'processes': [
{'name': 'chrome', 'pid': 812, 'rss_mb': 800.0},
{'name': 'python', 'pid': 421, 'rss_mb': 150.0},
{'name': 'vim', 'pid': 111, 'rss_mb': 20.0},
],
}
def test_make_process_table_returns_table():
table = make_process_table(SAMPLE_SNAPSHOT)
assert isinstance(table, Table)
def test_make_process_table_has_three_columns():
table = make_process_table(SAMPLE_SNAPSHOT)
assert len(table.columns) == 3
def test_make_process_table_high_rss_is_red():
table = make_process_table(SAMPLE_SNAPSHOT)
buf = StringIO()
c = Console(file=buf, markup=True, highlight=False)
c.print(table)
output = buf.getvalue()
assert 'red' in output.lower() or '๐ด' in output
def test_make_process_table_uses_rich_markup_not_html():
import inspect
src = inspect.getsource(make_process_table)
assert '<red>' not in src, 'Use Rich markup [red]...[/] not HTML <red>'
assert '[' in src, 'Use Rich markup [color]...[/]'
def test_print_header_uses_rich_markup():
import inspect
src = inspect.getsource(print_header)
assert '<' not in src.split('console.print')[1].split('\n')[0], (
'Use Rich markup [color]...[/] not HTML tags'
)
def test_live_not_created_inside_loop():
import inspect
src = inspect.getsource(run_live_dashboard)
assert src.count('with Live') == 1
# The while loop should be INSIDE the with Live block
live_pos = src.index('with Live')
while_pos = src.index('while')
assert while_pos > live_pos, 'while loop must be inside with Live()'