Aller au contenu
Développement medium

Rich : affichage riche dans le terminal Python

50 min de lecture

logo python

Rich transforme votre terminal en interface graphique. Couleurs, tableaux, barres de progression, arbres, logs enrichis, le tout dans un simple terminal. Ce guide vous fait construire pas à pas un vrai outil : un script de health check d'infrastructure qui scanne des services, affiche les résultats dans un tableau coloré, avec une barre de progression et des logs structurés. À chaque étape, vous découvrez une fonctionnalité de Rich parce que le script en a besoin.

Prérequis : savoir écrire des fonctions Python et utiliser print() (voir le guide sur les fonctions).

Rich est aussi le moteur de rendu de Textual, le framework TUI. Tout ce que vous apprenez ici sur le markup et les styles s'applique directement dans Textual (voir le guide Textual).

  • Remplacer print() par une Console Rich pour des sorties nettes.
  • Colorer le texte avec le balisage (markup) Rich.
  • Structurer des résultats en tables et en arbres.
  • Ajouter des barres de progression aux scripts longs.
  • Enrichir les logs avec RichHandler et les tracebacks.

Le projet fil rouge : un health check d'infrastructure

Section intitulée « Le projet fil rouge : un health check d'infrastructure »

Imaginez que vous gérez plusieurs services (un serveur web, une API, une base de données). Vous voulez un script qui :

  1. Scanne chaque service pour vérifier s'il répond
  2. Affiche les résultats de façon lisible dans le terminal
  3. Montre la progression du scan en temps réel
  4. Logue les événements importants avec horodatage

On va commencer avec un print() basique, puis améliorer le script section par section avec Rich. À la fin, vous aurez un outil complet et réutilisable.

Voici le squelette de départ, un simple script avec print() :

"""health_check.py - Version de départ (sans Rich)."""
import random
import time
# Nos services à surveiller
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090},
]
def scanner_service(service):
"""Simule un ping vers un service et renvoie la latence."""
time.sleep(0.3) # Simule le temps réseau
latence = random.randint(1, 50)
ok = latence < 100
return {"latence": latence, "ok": ok}
# Scanner et afficher
print("=== Health Check ===")
for svc in services:
resultat = scanner_service(svc)
statut = "OK" if resultat["ok"] else "DOWN"
print(f" {svc['nom']:20s} {svc['ip']:15s} {resultat['latence']:>4}ms {statut}")
print("=== Terminé ===")

Ça fonctionne, mais la sortie est monotone : tout est en blanc sur fond noir, pas de structure visuelle, impossible de distinguer les succès des échecs d'un coup d'œil. On va corriger ça.

Rich est une bibliothèque Python pure, sans extension compilée : l'installation ne demande ni compilateur ni en-têtes système, et fonctionne à l'identique sur Linux, macOS et Windows. Épinglez la version dès maintenant, le rendu de certains composants évolue entre versions majeures et un script figé garde alors la même sortie.

Fenêtre de terminal
pip install rich==15.0.0

Vérifiez que l'installation fonctionne en lançant la démo intégrée :

Fenêtre de terminal
python -m rich

Si vous voyez des couleurs, des tableaux et des emojis dans le terminal, c'est bon.

Le premier pas avec Rich, c'est de remplacer print() par Console.print(). L'objet Console est le point d'entrée de toute utilisation de Rich :

from rich.console import Console
console = Console()
# Au lieu de print(), on utilise console.print()
console.print("=== Health Check ===")

Ça donne le même résultat, mais Console sait faire beaucoup plus. Par exemple, il détecte et colore automatiquement les adresses IP, les nombres, les URLs et les booléens :

console.print("Serveur : 10.0.1.10, port : 8080, latence : 12ms")
# → les nombres et l'IP apparaissent en couleur automatiquement

Console dispose aussi de méthodes utiles qu'on va exploiter dans notre script :

# rule() : crée un séparateur visuel - idéal pour structurer la sortie
console.rule("Health Check")
# log() : affiche un message avec l'heure et le fichier source
console.log("Scan démarré")
# → [14:32:01] Scan démarré health_check.py:8
# print_json() : affiche du JSON formaté et coloré
console.print_json('{"nom": "web-1", "cpu": 4, "ram_gb": 16}')

Mettons à jour notre script pour utiliser Console :

"""health_check.py - Étape 1 : Console."""
import random
import time
from rich.console import Console
console = Console()
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090},
]
def scanner_service(service):
time.sleep(0.3)
latence = random.randint(1, 50)
return {"latence": latence, "ok": latence < 100}
console.rule("Health Check")
for svc in services:
resultat = scanner_service(svc)
statut = "OK" if resultat["ok"] else "DOWN"
console.print(f" {svc['nom']:20s} {svc['ip']:15s} {resultat['latence']:>4}ms {statut}")
console.rule("Terminé")

C'est déjà plus lisible grâce aux séparateurs rule(), et les IP et nombres sont colorés. Mais les statuts OK/DOWN sont toujours en blanc. On veut du vert pour OK et du rouge pour DOWN.

Rich utilise une syntaxe de markup inspirée du BBCode, des balises entre crochets [], pour appliquer des couleurs et des styles au texte. La syntaxe est simple : [style]texte[/style].

Voici les styles de base :

from rich.console import Console
console = Console()
# Styles de texte
console.print("[bold]Gras[/bold]")
console.print("[italic]Italique[/italic]")
console.print("[underline]Souligné[/underline]")
console.print("[dim]Atténué[/dim]")
# Couleurs
console.print("[red]Rouge[/red]")
console.print("[green]Vert[/green]")
console.print("[yellow]Jaune[/yellow]")
console.print("[blue on white]Bleu sur fond blanc[/blue on white]")
# Combinaisons - les styles se cumulent
console.print("[bold red]Gras et rouge[/bold red]")
console.print("[bold green on black]Gras vert sur fond noir[/bold green on black]")

Le raccourci [/] ferme tous les styles ouverts d'un coup :

console.print("[bold red underline]Style complexe[/]") # tout fermé

Rich accepte aussi les couleurs hexadécimales et RGB :

console.print("[#ff8800]Orange hexadécimal[/#ff8800]")
console.print("[rgb(100,200,50)]Vert RGB[/rgb(100,200,50)]")

Et les emojis via leur code :

console.print(":white_check_mark: Service opérationnel")
console.print(":warning: Attention : espace disque faible")
console.print(":cross_mark: Service inaccessible")

Ces styles se combinent librement dans une même balise, séparés par des espaces : [bold red on white] cumule les trois. Le rendu réel dépend cependant du terminal, tous n'implémentent pas strike ni dim ; en cas de doute, python -m rich affiche l'échantillon complet dans votre terminal. La ligne link est la plus méconnue : elle produit un lien cliquable dans les terminaux compatibles, et se dégrade en texte simple ailleurs.

StyleSyntaxeRendu
Gras[bold]...[/bold]Texte en gras
Italique[italic]...[/italic]Texte en italique
Souligné[underline]...[/underline]Texte souligné
Barré[strike]...[/strike]Texte barré
Atténué[dim]...[/dim]Texte grisé
Inversé[reverse]...[/reverse]Couleurs inversées
Couleur texte[red]...[/red]Texte en rouge
Couleur fond[on blue]...[/on blue]Fond bleu
Lien[link=URL]...[/link]Lien cliquable

Quand vous utilisez les mêmes couleurs à plusieurs endroits, plutôt que de répéter le markup, définissez des objets Style réutilisables :

from rich.style import Style
STYLE_OK = Style(color="green", bold=True)
STYLE_DOWN = Style(color="red", bold=True)
STYLE_WARN = Style(color="yellow", bold=True)
console.print("Service opérationnel", style=STYLE_OK)
console.print("Service inaccessible", style=STYLE_DOWN)

Appliquons le markup à notre script de health check :

"""health_check.py - Étape 2 : markup couleur."""
import random
import time
from rich.console import Console
console = Console()
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090},
]
def scanner_service(service):
time.sleep(0.3)
latence = random.randint(1, 50)
return {"latence": latence, "ok": latence < 100}
console.rule("[bold blue]Health Check")
for svc in services:
resultat = scanner_service(svc)
if resultat["ok"]:
statut = "[bold green]OK[/bold green]"
style_latence = "green"
else:
statut = "[bold red]DOWN[/bold red]"
style_latence = "red"
latence = resultat["latence"]
console.print(
f" [cyan]{svc['nom']:20s}[/cyan] "
f"[dim]{svc['ip']:15s}[/dim] "
f"[{style_latence}]{latence:>4}ms[/{style_latence}] "
f"{statut}"
)
console.rule("[bold blue]Terminé")

Maintenant, les noms de services sont en cyan, les IP en gris, la latence est colorée, et le statut est en vert (OK) ou rouge (DOWN). En un coup d'œil, vous identifiez les problèmes. Mais la sortie ressemble encore à une liste de lignes. On peut faire mieux avec une table.

Rich peut afficher des tables formatées directement dans le terminal, avec des colonnes alignées, des bordures et des styles par colonne. On crée une table en trois étapes : instancier, ajouter les colonnes, ajouter les lignes.

from rich.table import Table
# 1. Créer la table
table = Table(title="État des services")
# 2. Ajouter les colonnes
table.add_column("Service", style="cyan")
table.add_column("Statut", justify="right")
# 3. Ajouter les lignes
table.add_row("web-frontend", "OK")
table.add_row("db-primary", "DOWN")
# 4. Afficher
console.print(table)

Les options se passent à add_column(), colonne par colonne, et pilotent surtout la façon dont Rich répartit la largeur disponible. Par défaut la table s'adapte à la largeur du terminal et coupe le contenu trop long. Les deux options qui changent vraiment le rendu sont no_wrap, qui interdit le retour à la ligne au prix d'une troncature, et justify, à passer en right pour toute colonne numérique afin que les chiffres s'alignent sur les unités.

OptionDescriptionDéfaut
styleCouleur/style du contenuAucun
justifyAlignement (left, center, right)left
no_wrapEmpêcher le retour à la ligneFalse
widthLargeur fixe en caractèresAuto
min_width / max_widthLimites de largeurAucun
header_styleStyle de l'en-têteAucun

Le style de bordure se choisit à la construction de la table, via une constante du module box. Ce n'est pas qu'une question d'esthétique : les traits épais et doubles utilisent des caractères Unicode que certains terminaux ou polices rendent mal. Pour une sortie destinée à être copiée dans un ticket ou un fichier texte, box=None reste le choix le plus sûr.

from rich import box
table = Table(box=box.ROUNDED) # Coins arrondis (le plus courant)
table = Table(box=box.SIMPLE) # Lignes simples
table = Table(box=box.HEAVY) # Traits épais
table = Table(box=box.DOUBLE) # Double ligne
table = Table(box=None) # Sans bordure

Intégrons la table à notre script :

"""health_check.py - Étape 3 : résultats en table."""
import random
import time
from rich.console import Console
from rich.table import Table
from rich import box
console = Console()
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090},
]
def scanner_service(service):
time.sleep(0.3)
latence = random.randint(1, 50)
return {"latence": latence, "ok": latence < 100}
# Scanner tous les services
resultats = []
for svc in services:
resultat = scanner_service(svc)
resultats.append({**svc, **resultat})
# Construire la table
table = Table(title="Health Check", box=box.ROUNDED, show_lines=True)
table.add_column("Service", style="cyan bold", no_wrap=True)
table.add_column("IP", style="dim")
table.add_column("Port", justify="right")
table.add_column("Latence", justify="right")
table.add_column("Statut", justify="center")
for r in resultats:
style_latence = "green" if r["latence"] < 20 else "yellow" if r["latence"] < 50 else "red"
statut = "[bold green]OK[/]" if r["ok"] else "[bold red]DOWN[/]"
table.add_row(
r["nom"],
r["ip"],
str(r["port"]),
f"[{style_latence}]{r['latence']}ms[/]",
statut,
)
console.print(table)

Résultat dans le terminal :

Health Check
╭─────────────────┬────────────┬──────┬─────────┬────────╮
│ Service │ IP │ Port │ Latence │ Statut │
├─────────────────┼────────────┼──────┼─────────┼────────┤
│ web-frontend │ 10.0.1.10 │ 80 │ 12ms │ OK │
├─────────────────┼────────────┼──────┼─────────┼────────┤
│ api-backend │ 10.0.2.10 │ 8080 │ 8ms │ OK │
├─────────────────┼────────────┼──────┼─────────┼────────┤
│ db-primary │ 10.0.3.10 │ 5432 │ 3ms │ OK │
├─────────────────┼────────────┼──────┼─────────┼────────┤
│ cache-redis │ 10.0.4.10 │ 6379 │ 42ms │ OK │
├─────────────────┼────────────┼──────┼─────────┼────────┤
│ monitoring │ 10.0.5.10 │ 9090 │ 28ms │ OK │
╰─────────────────┴────────────┴──────┴─────────┴────────╯

Les données sont structurées, alignées, colorées. Mais quand le scan prend du temps (beaucoup de services), l'utilisateur ne voit rien pendant l'attente. Il faut une barre de progression.

Rich propose deux façons d'afficher une barre de progression :

track() est la solution en une ligne. Vous l'utilisez à la place d'un itérable dans une boucle for :

from rich.progress import track
for item in track(range(100), description="Traitement..."):
time.sleep(0.02) # votre traitement ici
Traitement... ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 67% 0:00:01

Progress() permet de gérer plusieurs tâches et de personnaliser l'affichage :

from rich.progress import Progress
with Progress() as progress:
tache1 = progress.add_task("[cyan]Téléchargement...", total=100)
tache2 = progress.add_task("[green]Installation...", total=100)
while not progress.finished:
progress.update(tache1, advance=3)
progress.update(tache2, advance=1)
time.sleep(0.02)
Téléchargement... ━━━━━━━━━━━━━━━━━━━━━━━━ 100% 0:00:00
Installation... ━━━━━━━━━━━━━━━━ 45% 0:00:02

Intégrons track() à notre script pour montrer la progression du scan :

"""health_check.py - Étape 4 : barre de progression."""
import random
import time
from rich.console import Console
from rich.table import Table
from rich.progress import track
from rich import box
console = Console()
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090},
]
def scanner_service(service):
time.sleep(0.3)
latence = random.randint(1, 50)
return {"latence": latence, "ok": latence < 100}
# Scanner avec barre de progression
resultats = []
for svc in track(services, description="[cyan]Scan des services..."):
resultat = scanner_service(svc)
resultats.append({**svc, **resultat})
console.print()
# Table des résultats
table = Table(title="Health Check", box=box.ROUNDED, show_lines=True)
table.add_column("Service", style="cyan bold", no_wrap=True)
table.add_column("IP", style="dim")
table.add_column("Port", justify="right")
table.add_column("Latence", justify="right")
table.add_column("Statut", justify="center")
for r in resultats:
style_latence = "green" if r["latence"] < 20 else "yellow" if r["latence"] < 50 else "red"
statut = "[bold green]OK[/]" if r["ok"] else "[bold red]DOWN[/]"
table.add_row(
r["nom"], r["ip"], str(r["port"]),
f"[{style_latence}]{r['latence']}ms[/]", statut,
)
console.print(table)

Maintenant, l'utilisateur voit une barre progresser pendant le scan, puis le tableau de résultats. Le script devient réactif. Prochaine amélioration : on veut montrer l'architecture des services avant de les scanner.

Tree affiche des données hiérarchiques, comme l'architecture d'un cluster, l'arborescence de fichiers, ou la structure d'une organisation :

from rich.tree import Tree
# Créer l'arbre racine
arbre = Tree(":cloud: Mon infrastructure")
# Ajouter des branches
frontend = arbre.add("[green]Frontend")
frontend.add("[cyan]web-1 (10.0.1.10)")
frontend.add("[cyan]web-2 (10.0.1.11)")
backend = arbre.add("[yellow]Backend")
backend.add("[cyan]api-1 (10.0.2.10)")
console.print(arbre)
☁ Mon infrastructure
├── Frontend
│ ├── web-1 (10.0.1.10)
│ └── web-2 (10.0.1.11)
└── Backend
└── api-1 (10.0.2.10)

Panel encadre n'importe quel contenu Rich dans un cadre avec titre :

from rich.panel import Panel
console.print(Panel(
"Contenu encadré",
title="Mon titre",
subtitle="Mon sous-titre",
border_style="blue",
))
# Panel ajusté au contenu
console.print(Panel.fit("Texte court", title="Info"))

On combine Tree et Panel pour afficher l'architecture avant le scan :

"""health_check.py - Étape 5 : architecture avec Tree et Panel."""
import random
import time
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.tree import Tree
from rich.progress import track
from rich import box
console = Console()
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80, "groupe": "Frontend"},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080, "groupe": "Backend"},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432, "groupe": "Base de données"},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379, "groupe": "Cache"},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090, "groupe": "Observabilité"},
]
def scanner_service(service):
time.sleep(0.3)
latence = random.randint(1, 50)
return {"latence": latence, "ok": latence < 100}
# 1. Afficher l'architecture
arbre = Tree(":cloud: Infrastructure de production")
groupes = {}
for svc in services:
groupe = svc["groupe"]
if groupe not in groupes:
groupes[groupe] = arbre.add(f"[bold]{groupe}[/]")
groupes[groupe].add(f"[cyan]{svc['nom']}[/] ([dim]{svc['ip']}:{svc['port']}[/])")
console.print(Panel(arbre, title="Architecture", border_style="blue"))
console.print()
# 2. Scanner avec barre de progression
resultats = []
for svc in track(services, description="[cyan]Scan des services..."):
resultat = scanner_service(svc)
resultats.append({**svc, **resultat})
console.print()
# 3. Table des résultats
table = Table(title="Résultats du Health Check", box=box.ROUNDED, show_lines=True)
table.add_column("Service", style="cyan bold", no_wrap=True)
table.add_column("IP", style="dim")
table.add_column("Latence", justify="right")
table.add_column("Statut", justify="center")
for r in resultats:
style_latence = "green" if r["latence"] < 20 else "yellow" if r["latence"] < 50 else "red"
statut = "[bold green]OK[/]" if r["ok"] else "[bold red]DOWN[/]"
table.add_row(r["nom"], r["ip"], f"[{style_latence}]{r['latence']}ms[/]", statut)
console.print(table)
console.print()
# 4. Résumé encadré
total = len(resultats)
ok_count = sum(1 for r in resultats if r["ok"])
couleur = "green" if ok_count == total else "yellow" if ok_count > 0 else "red"
console.print(Panel.fit(
f"[bold]{ok_count}/{total}[/] services opérationnels\n"
f"Latence moyenne : [cyan]{sum(r['latence'] for r in resultats) / total:.0f}ms[/]",
title="Résumé",
border_style=couleur,
))

Le script affiche maintenant quatre sections : l'architecture en arbre, la barre de progression du scan, le tableau de résultats, et un résumé encadré. C'est déjà un outil utilisable. Mais si quelque chose plante, on veut des logs lisibles et des tracebacks utiles.

Python dispose d'un module logging intégré pour écrire des messages de log. Par défaut, les logs sont en texte brut sans couleurs. RichHandler remplace le handler par défaut pour afficher les logs avec horodatage, couleurs par niveau, et le nom du fichier source :

import logging
from rich.logging import RichHandler
# Configuration en une ligne
logging.basicConfig(
level=logging.DEBUG,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)]
)
log = logging.getLogger("health_check")
log.debug("Détail technique pour le debug")
log.info("Scan démarré")
log.warning("Latence élevée sur cache-redis")
log.error("Service db-primary inaccessible")
log.critical("Aucun service ne répond !")
[14:32:01] DEBUG Détail technique pour le debug health_check.py:12
[14:32:01] INFO Scan démarré health_check.py:13
[14:32:01] WARNING Latence élevée sur cache-redis health_check.py:14
[14:32:01] ERROR Service db-primary inaccessible health_check.py:15
[14:32:01] CRITICAL Aucun service ne répond ! health_check.py:16

Chaque niveau a sa couleur (DEBUG gris, INFO bleu, WARNING jaune, ERROR rouge, CRITICAL rouge gras), et Rich affiche automatiquement le fichier et le numéro de ligne.

Regardez d'abord la colonne des valeurs par défaut : rich_tracebacks et tracebacks_show_locals sont à False, alors que ce sont précisément les deux options qui rendent RichHandler utile en dépannage. Sans elles, une exception loguée par log.exception() s'affiche avec le traceback Python brut, et vous perdez le bénéfice de la bibliothèque au moment exact où vous en avez besoin.

OptionDescriptionDéfaut
rich_tracebacksTracebacks enrichis et colorésFalse
show_timeAfficher l'heureTrue
show_levelAfficher le niveau (INFO, ERROR...)True
show_pathAfficher fichier:ligneTrue
markupInterpréter le markup Rich dans les messagesFalse
tracebacks_show_localsAfficher les variables locales dans les tracebacksFalse

Quand un script plante, le traceback Python standard est souvent difficile à lire. Rich le remplace par une version colorée avec le code source et les variables locales :

from rich.traceback import install
# Active les tracebacks Rich pour tout le script
install(show_locals=True)
Traceback (most recent call last):
File "health_check.py", line 12, in <module>
result = process_config(data)
File "health_check.py", line 8, in process_config
return config["database"]["host"]
KeyError: 'database'

La version Rich montre le code source autour de l'erreur, les variables locales à chaque niveau, et une coloration syntaxique qui aide à repérer le problème immédiatement. Activez-le au début de vos scripts pour ne plus jamais revenir en arrière.

Pour indiquer qu'un travail est en cours sans barre de progression (quand vous ne connaissez pas le nombre d'étapes), utilisez console.status() :

with console.status("[bold green]Connexion au cluster...") as status:
time.sleep(2)
status.update("[bold yellow]Vérification des certificats...")
time.sleep(1)
status.update("[bold cyan]Chargement de la config...")
time.sleep(1)
console.print("[bold green]Connecté !")

Un spinner animé tourne dans le terminal pendant chaque étape.

Live permet de mettre à jour un affichage (table, panel...) sans scintillement ni défilement. L'affichage se rafraîchit sur place :

from rich.live import Live
def generer_table(etape):
"""Génère une table de statut avec l'état actuel du scan."""
table = Table(title="Scan en cours")
table.add_column("Service")
table.add_column("Statut")
noms = ["web-frontend", "api-backend", "db-primary", "cache-redis", "monitoring"]
for i, nom in enumerate(noms):
if i < etape:
table.add_row(nom, "[green]Scanné")
elif i == etape:
table.add_row(nom, "[yellow]En cours...")
else:
table.add_row(nom, "[dim]En attente")
return table
with Live(generer_table(0), console=console, refresh_per_second=4) as live:
for etape in range(5):
time.sleep(0.5)
live.update(generer_table(etape + 1))

La table se met à jour en temps réel : chaque service passe de "En attente" à "En cours" puis "Scanné", le tout sans que le terminal défile.

Pendant le développement, vous pouvez utiliser rich.inspect() pour explorer n'importe quel objet Python, ses attributs, ses méthodes, sa documentation :

from rich import inspect
# Inspecter un dictionnaire
inspect({"nom": "web-1", "ip": "10.0.1.10"})
# Inspecter avec les méthodes visibles
inspect(["a", "b", "c"], methods=True)
# Inspecter un module entier
import os
inspect(os, methods=True)
╭──────────────── <class 'dict'> ────────────────╮
│ {'nom': 'web-1', 'ip': '10.0.1.10'} │
│ │
│ clear = def clear(...) │
│ copy = def copy(...) │
│ keys = def keys(...) │
│ values = def values(...) │
│ items = def items(...) │
│ ... │
╰─────────────────────────────────────────────────╯

Voici le script complet qui rassemble toutes les fonctionnalités vues dans ce guide. Vous pouvez le copier et l'adapter à votre propre infrastructure :

"""health_check.py - Script complet de health check avec Rich."""
import logging
import random
import time
from rich.console import Console
from rich.logging import RichHandler
from rich.panel import Panel
from rich.progress import track
from rich.table import Table
from rich.traceback import install
from rich.tree import Tree
from rich import box
# --- Configuration ---
install(show_locals=True) # Tracebacks Rich pour tout le script
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
datefmt="[%X]",
handlers=[RichHandler(rich_tracebacks=True)],
)
log = logging.getLogger("health_check")
console = Console()
# --- Données ---
services = [
{"nom": "web-frontend", "ip": "10.0.1.10", "port": 80, "groupe": "Frontend"},
{"nom": "api-backend", "ip": "10.0.2.10", "port": 8080, "groupe": "Backend"},
{"nom": "db-primary", "ip": "10.0.3.10", "port": 5432, "groupe": "Base de données"},
{"nom": "cache-redis", "ip": "10.0.4.10", "port": 6379, "groupe": "Cache"},
{"nom": "monitoring", "ip": "10.0.5.10", "port": 9090, "groupe": "Observabilité"},
]
def scanner_service(service):
"""Simule un health check réseau."""
time.sleep(0.3)
latence = random.randint(1, 80)
return {"latence": latence, "ok": latence < 100}
def afficher_architecture(services):
"""Affiche l'arborescence des services dans un panel."""
arbre = Tree(":cloud: Infrastructure de production")
groupes = {}
for svc in services:
groupe = svc["groupe"]
if groupe not in groupes:
groupes[groupe] = arbre.add(f"[bold]{groupe}[/]")
groupes[groupe].add(
f"[cyan]{svc['nom']}[/] ([dim]{svc['ip']}:{svc['port']}[/])"
)
console.print(Panel(arbre, title="Architecture", border_style="blue"))
def scanner_tous(services):
"""Scanne chaque service avec une barre de progression."""
resultats = []
for svc in track(services, description="[cyan]Scan des services..."):
resultat = scanner_service(svc)
resultats.append({**svc, **resultat})
if not resultat["ok"]:
log.error(f"{svc['nom']} est DOWN (latence : {resultat['latence']}ms)")
elif resultat["latence"] > 30:
log.warning(f"{svc['nom']} - latence élevée : {resultat['latence']}ms")
else:
log.info(f"{svc['nom']} - OK ({resultat['latence']}ms)")
return resultats
def afficher_resultats(resultats):
"""Affiche les résultats dans une table colorée."""
table = Table(
title="Résultats du Health Check",
box=box.ROUNDED,
show_lines=True,
)
table.add_column("Service", style="cyan bold", no_wrap=True)
table.add_column("Groupe", style="dim")
table.add_column("IP", style="dim")
table.add_column("Latence", justify="right")
table.add_column("Statut", justify="center")
for r in resultats:
if r["latence"] < 20:
style_latence = "green"
elif r["latence"] < 50:
style_latence = "yellow"
else:
style_latence = "red"
statut = "[bold green]OK[/]" if r["ok"] else "[bold red]DOWN[/]"
table.add_row(
r["nom"],
r["groupe"],
f"{r['ip']}:{r['port']}",
f"[{style_latence}]{r['latence']}ms[/]",
statut,
)
console.print(table)
def afficher_resume(resultats):
"""Affiche un résumé encadré."""
total = len(resultats)
ok_count = sum(1 for r in resultats if r["ok"])
latence_moy = sum(r["latence"] for r in resultats) / total
if ok_count == total:
couleur = "green"
icone = ":white_check_mark:"
elif ok_count > 0:
couleur = "yellow"
icone = ":warning:"
else:
couleur = "red"
icone = ":cross_mark:"
console.print(Panel.fit(
f"{icone} [bold]{ok_count}/{total}[/] services opérationnels\n"
f"Latence moyenne : [cyan]{latence_moy:.0f}ms[/]",
title="Résumé",
border_style=couleur,
))
# --- Exécution ---
console.rule("[bold blue]Health Check - Production")
console.print()
afficher_architecture(services)
console.print()
resultats = scanner_tous(services)
console.print()
afficher_resultats(resultats)
console.print()
afficher_resume(resultats)

Lancez-le :

Fenêtre de terminal
python health_check.py

Le terminal affiche successivement : l'arbre d'architecture, la barre de progression du scan, les logs horodatés par service, le tableau de résultats coloré, et le résumé encadré avec une icône verte/jaune/rouge.

Vous avez peut-être envie d'envoyer ce rapport par email ou de l'archiver. Rich peut exporter la sortie en texte brut ou en HTML :

# Activer l'enregistrement
console = Console(record=True)
# ... tout le script s'exécute ici ...
# Exporter en texte brut (sans codes couleur)
with open("rapport.txt", "w") as f:
f.write(console.export_text())
# Exporter en HTML (avec les couleurs préservées)
with open("rapport.html", "w") as f:
f.write(console.export_html())

Pour écrire directement dans un fichier sans couleurs :

with open("rapport.txt", "w") as f:
fichier_console = Console(file=f, force_terminal=False)
fichier_console.print("Rapport sans couleurs")

Ce tableau se lit par la colonne de gauche : partez du besoin réel de votre script, pas du composant qui vous plaît. La confusion la plus fréquente concerne les trois indicateurs d'attente. track() suppose que vous connaissez le nombre d'itérations, Progress() sert quand plusieurs traitements avancent en parallèle, et console.status() couvre le cas où la durée est inconnue. Choisir le mauvais des trois donne une barre bloquée à 0 % ou une progression qui saute d'un coup à 100 %.

BesoinFonctionnalité RichQuand l'utiliser
Afficher un résultatconsole.print() + markupToujours, remplace print()
Structurer des donnéesTableListes de serveurs, résultats, comparaisons
Montrer une hiérarchieTree + PanelArchitecture, arborescence fichiers, organisations
Suivre un traitement longtrack()Boucle dont vous connaissez le nombre d'itérations
Tâches parallèlesProgress()Plusieurs progressions simultanées
Attente indéterminéeconsole.status()Connexion, chargement, sans nombre d'étapes
Mise à jour en placeLiveDashboard, monitoring en temps réel
JournaliserRichHandlerTout script qui utilise logging
Débugger un crashinstall() de tracebackEn début de script, toujours
Explorer un objetinspect()En debug uniquement

Rich reste largement assez rapide pour un outil d'administration, mais il fait beaucoup plus de travail que print() : analyse du markup, calcul des largeurs, génération des séquences ANSI. Les trois points ci-dessous couvrent les seuls cas où cela se remarque, tous liés à un volume inhabituel de sorties ou à de la mémoire retenue inutilement.

  • Console(record=True) : active l'enregistrement uniquement si vous exportez, ça consomme de la mémoire.
  • track() : préférez-le à Progress() pour les cas simples.
  • Markup dans les boucles serrées : pour des millions d'itérations, print() natif reste plus rapide.

Rich adapte sa sortie à la destination : il interroge le terminal pour connaître sa largeur et son niveau de couleur, et bascule en texte simple quand la sortie est redirigée. Ce comportement est souhaitable en production mais déroutant en CI, où les logs arrivent sans la moindre couleur alors que tout fonctionne en local. Les trois réglages ci-dessous couvrent ces situations.

  • Rich détecte automatiquement si le terminal supporte les couleurs.
  • En CI/CD : Rich désactive les couleurs si stdout n'est pas un TTY. Force-les avec Console(force_terminal=True).
  • Redirection vers un fichier : utilisez Console(file=f) pour un export propre sans codes ANSI.

Presque tous les problèmes rencontrés avec Rich viennent d'un décalage entre ce que la bibliothèque croit de son terminal et la réalité : largeur, support des couleurs, présence d'un TTY, police avec ou sans emoji. La deuxième cause, plus banale, est un print() natif resté quelque part dans le code, qui affiche le markup tel quel au lieu de l'interpréter.

SymptômeCause probableSolution
Pas de couleurs dans le terminalTerminal ne supporte pas les couleursVérifiez avec python -m rich
Couleurs absentes en CI/CDstdout n'est pas un TTYConsole(force_terminal=True)
Texte tronqué dans les tablesTerminal trop étroitConsole(width=120) ou réduisez les colonnes
[red]texte[/red] affiché tel quelVous utilisez print() au lieu de console.print()Remplacez par Console().print()
Markup interprété dans les logsmarkup=True sur RichHandlerMettez markup=False si vous loguez des données brutes
Emoji non affichéTerminal sans support emojiConsole(emoji=False)
Export HTML sans couleursOubli de record=TrueConsole(record=True) avant les print()

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Rich remplace print() par Console.print(), couleurs, markup, détection automatique d'IP/URL, et export.
  • Le markup [bold red]texte[/] applique des styles combinables en une syntaxe simple.
  • Les tables structurent les résultats avec colonnes alignées, bordures et couleurs par colonne.
  • track() ajoute une barre de progression en une seule ligne de changement.
  • Tree et Panel affichent des hiérarchies et encadrent les résultats, pour un rendu professionnel.
  • RichHandler remplace le logging standard avec horodatage, couleurs par niveau et fichier source.
  • install() de rich.traceback rend les erreurs lisibles avec code source et variables locales.
  • Rich est le moteur de Textual : le markup et les styles appris ici s'appliquent dans les TUI.
  • Le fil rouge : chaque fonctionnalité Rich résout un vrai besoin du script, couleur pour distinguer OK/DOWN, table pour structurer, progression pour l'attente, logs pour tracer, tracebacks pour débugger.
  • Scanner un réseau avec nmap : remplacez le scan simulé du fil rouge par de vraies sondes réseau, puis affichez les résultats dans une table Rich.
  • Fabric : automatiser en SSH : combinez Rich avec l'exécution de commandes à distance pour un tableau de bord d'administration coloré.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn