Módulo 2 · Lección 24 de 28
rich: salida con estilo
La primera biblioteca externa del curso: colores, paneles, reglas y volcado de objetos sin escribir un solo código ANSI.
En las lecciones anteriores montaste a mano los colores de la terminal con códigos ANSI, las tablas con f-strings alineadas y las barras de progreso contando caracteres. Funciona, y ha merecido la pena hacerlo una vez: ahora sabes qué hay debajo. Pero también sabes lo que cuesta. Una tabla con anchos que se ajusten al contenido son cuarenta líneas, y un color mal cerrado te deja la terminal en rojo hasta que la cierras.
rich es una biblioteca que hace todo eso. No inventa nada que no pudieras escribir tú; lo que hace es que no tengas que escribirlo. Es la primera biblioteca externa del curso, y es una buena primera porque no tiene dependencias raras, funciona en los tres sistemas operativos y se nota desde la primera línea.
Instalarla#
Con el entorno virtual del proyecto activado:
pip install richCollecting rich
Using cached rich-15.0.0-py3-none-any.whl.metadata (18 kB)
Collecting markdown-it-py>=2.2.0 (from rich)
Using cached markdown_it_py-4.2.0-py3-none-any.whl.metadata (7.4 kB)
Collecting pygments<3.0.0,>=2.13.0 (from rich)
Using cached pygments-2.21.0-py3-none-any.whl.metadata (2.5 kB)
Collecting mdurl~=0.1 (from markdown-it-py>=2.2.0->rich)
Using cached mdurl-0.1.2-py3-none-any.whl.metadata (1.6 kB)
Installing collected packages: pygments, mdurl, markdown-it-py, rich
Successfully installed markdown-it-py-4.2.0 mdurl-0.1.2 pygments-2.21.0 rich-15.0.0Cuatro paquetes: rich y tres dependencias suyas. pygments es el que colorea el código fuente y markdown-it-py el que interpreta Markdown, dos cosas que verás en la lección siguiente.
Si tu herramienta va a usar rich, acuérdate de declararlo en el pyproject.toml, porque si no, quien la instale no lo tendrá:
dependencies = [
"rich>=13.0",
]El objeto Console#
Todo en rich gira alrededor de una Console. Se crea una vez y se usa en todo el programa, y su método print sustituye al print normal:
from rich.console import Console
consola = Console()
consola.print("Hola desde rich")
consola.print("Un texto [bold]en negrita[/bold] y otro [italic]en cursiva[/italic]")
consola.print("Peligro: [bold red]saldo negativo[/bold red]")
consola.print("Fondo y color: [white on blue] RESUMEN [/white on blue]")
consola.print("Cerrar sin nombre: [bold]así también vale[/]")Hola desde rich
Un texto en negrita y otro en cursiva
Peligro: saldo negativo
Fondo y color: RESUMEN
Cerrar sin nombre: así también valeEl marcado entre corchetes#
Eso de [bold red]...[/bold red] es el marcado de rich, y funciona como el HTML: una etiqueta de apertura entre corchetes, el texto, y una de cierre con una barra delante. La etiqueta de cierre puede ir vacía, [/], y cierra la última que se abriera.
Dentro de la etiqueta caben varias palabras separadas por espacios, y rich las combina:
| Etiqueta | Efecto |
|---|---|
[bold] | Negrita |
[italic] | Cursiva |
[underline] | Subrayado |
[strike] | Tachado |
[dim] | Atenuado |
[reverse] | Colores invertidos |
[red] | Color de letra |
[on blue] | Color de fondo |
[bold red on white] | Todo a la vez |
La ventaja sobre los códigos ANSI que escribiste a mano no es solo que se lea mejor. Es que rich cierra siempre: aunque tu programa muera en mitad de una línea, la terminal no se queda pintada de rojo, porque el estilo se aplica por trozos y no como un interruptor global.
Escapar los corchetes#
El precio de ese marcado es que los corchetes pasan a ser especiales. Si imprimes un texto que los contiene, rich intentará interpretarlos:
from rich.console import Console
from rich.markup import escape
consola = Console()
etiquetas = "[red] y [bold]"
# rich se come los corchetes: cree que son estilos.
consola.print(f"Etiquetas del gasto: {etiquetas}")
# Con la barra invertida delante del corchete de apertura se imprimen literales.
consola.print(r"Etiquetas del gasto: \[red] y \[bold]")
# escape() pone esas barras por ti. Es lo que se usa con datos de fuera.
consola.print(f"Etiquetas del gasto: {escape(etiquetas)}")
# O se apaga el marcado para esa línea.
consola.print(f"Etiquetas del gasto: {etiquetas}", markup=False)
print(escape("[red]"))Etiquetas del gasto: y
Etiquetas del gasto: [red] y [bold]
Etiquetas del gasto: [red] y [bold]
Etiquetas del gasto: [red] y [bold]
\[red]La primera línea ha perdido los corchetes y lo que había dentro: rich los ha tomado por estilos y los ha aplicado. Las tres soluciones son la barra invertida a mano, la función escape() y desactivar el marcado con markup=False.
Colores#
Hay tres formas de nombrar un color, y se pueden mezclar:
from rich.console import Console
consola = Console()
# Por nombre
consola.print("[green]Pagado[/green] · [yellow]Pendiente[/yellow] · [red]Vencido[/red]")
# Por número de la paleta de 256
consola.print("[color(208)]Naranja del 208[/color(208)]")
# En hexadecimal
consola.print("[#1e88e5]Azul corporativo #1e88e5[/#1e88e5]")
# Fondo en hexadecimal
consola.print("[bold white on #b71c1c] IMPORTE FUERA DE PRESUPUESTO [/]")
# Varios estilos a la vez
consola.print("[bold underline #00897b]Total del mes[/]")Pagado · Pendiente · Vencido
Naranja del 208
Azul corporativo #1e88e5
IMPORTE FUERA DE PRESUPUESTO
Total del mes- Por nombre hay unos ciento cincuenta: los ocho de siempre (
black,red,green,yellow,blue,magenta,cyan,white), sus versiones brillantes con el prefijobright_, y una lista larga con nombres comodeep_sky_blue1odark_orange3. - Por número, con
color(N)y un número del 0 al 255, que es la paleta estándar de las terminales. - En hexadecimal, con la misma notación que en la web. Es la que usarás si tienes unos colores de marca.
Para ver la lista completa de nombres en tu propia terminal:
python -m rich.colorEl resaltado automático#
rich mira lo que imprimes y colorea solo los números, las cadenas entre comillas, las rutas, las direcciones web, los True, los False y los None. Se llama resaltado automático y está activado por defecto:
from rich.console import Console
consola = Console()
consola.print("He apuntado 4 gastos por 76.20 euros en /home/erwin/gastos.json")
consola.print({"comida": 55.8, "ocio": 18.0, "transporte": 2.4})
consola.print("Sin resaltar: 4 gastos, 76.20 euros", highlight=False)
sin_resaltado = Console(highlight=False)
sin_resaltado.print("Esta consola nunca resalta: 4 gastos, 76.20 euros")He apuntado 4 gastos por 76.20 euros en /home/erwin/gastos.json
{'comida': 55.8, 'ocio': 18.0, 'transporte': 2.4}
Sin resaltar: 4 gastos, 76.20 euros
Esta consola nunca resalta: 4 gastos, 76.20 eurosEn la terminal, las dos primeras líneas salen con los números y la ruta en colores distintos, y las dos últimas en blanco.
Es cómodo para depurar y molesto para los mensajes cuidados, porque te pinta de color cosas que no querías destacar. Se apaga en una llamada suelta con highlight=False o para toda la consola pasándoselo al constructor. En una herramienta con una salida diseñada, lo normal es crear la consola con el resaltado apagado y colorear a mano lo que interese.
rich.print como sustituto directo#
Si no quieres crear una consola, rich trae una función print que usa una consola global:
from rich import print
print("Hola desde [bold green]rich.print[/bold green]")
print({"comida": 55.8, "ocio": 18.0})
print([1, 2, 3], (True, None))Hola desde rich.print
{'comida': 55.8, 'ocio': 18.0}
[1, 2, 3]
(True, None)Ese from rich import print tapa el print normal en ese fichero, lo cual es a la vez su gracia y su peligro: te da color en todo el programa cambiando una línea, pero cualquiera que lea el fichero verá print(...) y pensará en el de siempre. Para un script corto está bien; en una herramienta con varios módulos, crea una Console y pásala por ahí, que se ve de dónde sale.
Justificar, recortar y ajustar al ancho#
rich conoce el ancho de la terminal y lo usa para colocar el texto:
from rich.console import Console
consola = Console(width=40)
concepto = "Compra semanal del supermercado del barrio"
consola.print("Centrado", justify="center")
consola.print("A la derecha", justify="right")
consola.print(concepto, justify="full")
consola.rule()
consola.print(concepto, overflow="ellipsis", no_wrap=True)
consola.print(concepto, overflow="crop", no_wrap=True)
consola.print(concepto) Centrado
A la derecha
Compra semanal del supermercado del
barrio
────────────────────────────────────────
Compra semanal del supermercado del bar…
Compra semanal del supermercado del barr
Compra semanal del supermercado del
barrioLo que hace cada opción:
justifycoloca el texto dentro del ancho:"left"por defecto,"center","right"y"full", que reparte espacios entre las palabras para que las dos márgenes queden rectos, como en un periódico.no_wrap=Trueimpide que el texto salte de línea.overflowdecide qué pasa con lo que sobra:"ellipsis"lo corta y pone puntos suspensivos,"crop"lo corta a secas y"fold", que es lo que hace por defecto, lo pasa a la línea siguiente.
El Console(width=40) del ejemplo fija el ancho para que la captura sea estable. En un programa de verdad no se pone: sin él, rich mide la terminal y usa su ancho real, y si la salida va a un fichero usa 80 columnas.
Reglas con rule#
Una línea horizontal, con un título opcional en medio, para separar secciones:
from rich.console import Console
consola = Console(width=60)
consola.rule()
consola.rule("Gastos de septiembre")
consola.rule("[bold red]Fuera de presupuesto", style="red")
consola.rule("A la izquierda", align="left")
consola.rule("Con otro carácter", characters="=")────────────────────────────────────────────────────────────
─────────────────── Gastos de septiembre ───────────────────
─────────────────── Fuera de presupuesto ───────────────────
A la izquierda ─────────────────────────────────────────────
==================== Con otro carácter =====================Es el equivalente a aquel print("-" * 60) que escribías a mano, pero midiendo la terminal sola y con el título centrado sin hacer cuentas.
Paneles#
Un Panel es un recuadro alrededor de un contenido. Sirve para destacar un mensaje, encuadrar un resumen o avisar de algo:
from rich.console import Console
from rich.panel import Panel
consola = Console(width=60)
consola.print(Panel("Se han apuntado 4 gastos."))
consola.print(Panel("Se han apuntado 4 gastos.", title="misgastos"))
consola.print(
Panel(
"Llevas [bold]76.20 €[/bold] gastados este mes.\n"
"El presupuesto eran 60.00 €.",
title="[bold red]Aviso",
subtitle="septiembre de 2026",
border_style="red",
padding=(1, 2),
)
)
consola.print(Panel("Solo lo que ocupa el texto", expand=False))╭──────────────────────────────────────────────────────────╮
│ Se han apuntado 4 gastos. │
╰──────────────────────────────────────────────────────────╯
╭─────────────────────── misgastos ────────────────────────╮
│ Se han apuntado 4 gastos. │
╰──────────────────────────────────────────────────────────╯
╭───────────────────────── Aviso ──────────────────────────╮
│ │
│ Llevas 76.20 € gastados este mes. │
│ El presupuesto eran 60.00 €. │
│ │
╰─────────────────── septiembre de 2026 ───────────────────╯
╭────────────────────────────╮
│ Solo lo que ocupa el texto │
╰────────────────────────────╯Los parámetros que vas a usar:
titleysubtitle, arriba y abajo, y admiten marcado.border_style, un estilo para el borde entero.padding, el hueco entre el borde y el contenido, como(vertical, horizontal).expand=False, para que el panel mida lo que mida el contenido en vez de ocupar el ancho entero.
Y un detalle importante: lo que le pasas a Panel no tiene que ser texto. Puede ser otro objeto de rich, por ejemplo una tabla, y lo verás en la lección siguiente.
Emojis#
rich traduce los nombres de emoji entre dos puntos, con la misma lista que usan otras herramientas:
from rich.console import Console
consola = Console(width=60)
consola.print(":white_check_mark: Gasto apuntado")
consola.print(":cross_mark: El importe debe ser positivo")
consola.print(":warning: Te quedan 3.80 € de presupuesto")
consola.print(":money_with_wings: :bar_chart: :file_folder: :hourglass:")
consola.print("Sin traducir: :white_check_mark:", emoji=False)✅ Gasto apuntado
❌ El importe debe ser positivo
⚠ Te quedan 3.80 € de presupuesto
💸 📊 📁 ⌛
Sin traducir: :white_check_mark:Para ver la lista entera:
python -m rich.emojiVolcar objetos de forma legible#
El print normal imprime un diccionario anidado en una sola línea kilométrica. El de rich lo reparte y lo indenta:
from rich import print
presupuesto = {
"mes": "septiembre",
"limite": 60.0,
"categorias": {
"comida": {"limite": 30.0, "gastado": 55.8},
"ocio": {"limite": 20.0, "gastado": 18.0},
},
"avisos": ["comida por encima del límite", "quedan 11 días"],
}
print(presupuesto){
'mes': 'septiembre',
'limite': 60.0,
'categorias': {
'comida': {'limite': 30.0, 'gastado': 55.8},
'ocio': {'limite': 20.0, 'gastado': 18.0}
},
'avisos': ['comida por encima del límite', 'quedan 11 días']
}Fíjate en que no lo rompe todo a lo tonto: los diccionarios pequeños caben en una línea y se quedan en una línea. rich mide antes de decidir.
Para inspeccionar un objeto por dentro está inspect, que enseña su documentación, sus atributos y sus métodos:
from dataclasses import dataclass
from rich import inspect
from rich.console import Console
consola = Console(width=70)
@dataclass
class Gasto:
"""Un gasto apuntado."""
concepto: str
importe: float
categoria: str = "otros"
def encarecer(self, porcentaje: float) -> float:
"""Devuelve el importe subido en ese porcentaje."""
return self.importe * (1 + porcentaje / 100)
inspect(Gasto("Café con leche", 1.60, "comida"), console=consola, methods=True)╭───────────────────── <class '__main__.Gasto'> ─────────────────────╮
│ Un gasto apuntado. │
│ │
│ ╭────────────────────────────────────────────────────────────────╮ │
│ │ Gasto( │ │
│ │ │ concepto='Café con leche', │ │
│ │ │ importe=1.6, │ │
│ │ │ categoria='comida' │ │
│ │ ) │ │
│ ╰────────────────────────────────────────────────────────────────╯ │
│ │
│ categoria = 'comida' │
│ concepto = 'Café con leche' │
│ importe = 1.6 │
│ encarecer = def encarecer(porcentaje: float) -> float: Devuelve el │
│ importe subido en ese porcentaje. │
╰────────────────────────────────────────────────────────────────────╯Ahí está todo: el docstring de la clase, el repr del objeto, sus tres atributos con sus valores y el método con su firma y su docstring. methods=True es lo que hace que aparezcan los métodos; sin él solo salen los datos.
inspect es una herramienta de exploración, no de programa terminado: sirve para mirar qué trae un objeto de una biblioteca que no conoces, y para eso es más rápido que buscar la documentación. Pruébalo con inspect(inspect) para ver todas sus opciones.
Excepciones legibles#
Un traceback de Python es útil pero incómodo de leer, sobre todo cuando tiene diez niveles. rich puede sustituirlo por uno con la línea del fallo destacada y el valor de las variables de cada marco:
from rich.console import Console
from rich.traceback import install
install(show_locals=True, width=76)
def calcular_media(importes):
return sum(importes) / len(importes)
consola = Console(width=76)
consola.print("Calculando la media de los gastos...")
print(calcular_media([]))python3 medias.pyCalculando la media de los gastos...
╭─────────────────── Traceback (most recent call last) ────────────────────╮
│ /Users/erwin/proyectos/misgastos/medias.py:13 in <module> │
│ │
│ 10 │
│ 11 consola = Console(width=76) │
│ 12 consola.print("Calculando la media de los gastos...") │
│ ❱ 13 print(calcular_media([])) │
│ 14 │
│ │
│ ╭───────────── locals ──────────────╮ │
│ │ consola = <console width=76 None> │ │
│ ╰───────────────────────────────────╯ │
│ │
│ /Users/erwin/proyectos/misgastos/medias.py:8 in calcular_media │
│ │
│ 5 │
│ 6 │
│ 7 def calcular_media(importes): │
│ ❱ 8 │ return sum(importes) / len(importes) │
│ 9 │
│ 10 │
│ 11 consola = Console(width=76) │
│ │
│ ╭─── locals ────╮ │
│ │ importes = [] │ │
│ ╰───────────────╯ │
╰──────────────────────────────────────────────────────────────────────────╯
ZeroDivisionError: division by zeroLa flecha ❱ marca la línea que falló, y alrededor se ven las de contexto con su número. Pero lo que de verdad ahorra tiempo es el recuadro locals: ahí está importes = [], que es la causa del problema, sin haber tenido que poner un solo print para averiguarlo.
Basta con llamar a install() una vez, al principio del programa. A partir de ahí, cualquier excepción que no captures se imprime así.