Saltar al contenido
🐍 Curso de Python
/

Módulo 2 · Lección 5 de 28

Colores y estilos en la terminal

Los códigos de escape ANSI explicados por dentro, un módulo propio para usarlos y cuándo hay que renunciar al color.

⏱ 20 min de lectura

Un programa que escupe veinte líneas del mismo gris obliga a leerlas todas para encontrar la que importa. Con el error en rojo y los avisos en amarillo, la línea que importa salta a la vista sin leer nada. Es la diferencia entre una herramienta que se usa a gusto y una que cansa.

El color en la terminal no lo pone ninguna función de Python: se pide metiendo unos caracteres especiales en el propio texto. La terminal los intercepta, los interpreta como órdenes y no los dibuja. Entender ese mecanismo lleva diez minutos y evita tratarlo como magia.

Anatomía de un código de escape#

Este es el código completo para escribir tres palabras en rojo:

python
rojo = "\033[31mtexto en rojo\033[0m"
print(rojo)

Y esto es lo que hay realmente dentro de esa cadena:

consola
>>> rojo = "\033[31mtexto en rojo\033[0m"
>>> rojo
'\x1b[31mtexto en rojo\x1b[0m'
>>> len(rojo)
22
>>> len("texto en rojo")
13

La cadena tiene 22 caracteres y solo 13 se ven en la pantalla. Los otros nueve son las dos órdenes, y se desglosan así:

TrozoQué es
\033El carácter de escape, el número 27 de la tabla ASCII. Es el que avisa a la terminal de que lo que viene no es texto
[Abre la secuencia. Escape más corchete se llama Control Sequence Introducer
31El parámetro. Aquí, "pon el texto en rojo"
mCierra la secuencia. La m significa que esto va de aspecto del texto

\033 es una forma de escribir el carácter 27 en octal, y verás las otras dos por ahí: \x1b en hexadecimal, que es la que usa Python al mostrar la cadena, y \u001b. Las tres son exactamente el mismo carácter:

consola
>>> ord("\033")
27
>>> "\033" == "\x1b" == "\u001b"
True

Por qué hay que cerrar con \033[0m#

La terminal no colorea una cadena: cambia de estado. En cuanto recibe \033[31m, todo lo que salga a partir de ahí es rojo, incluida la salida de tu programa una hora después, y el prompt de la terminal cuando el programa termine. \033[0m es el código que devuelve todo a la normalidad.

python
print("\033[31mesto sale en rojo")
print("y esto también, aunque no haya pedido nada")
print("\033[0mesto ya está normal")

Si ejecutas eso, las dos primeras líneas salen rojas. Es el error más común del principiante con colores y es fácil de reconocer: si tu prompt se queda de otro color después de ejecutar tu programa, te has dejado un \033[0m sin escribir. Se arregla ejecutando reset en la terminal.

Los colores de texto#

Ocho colores, con los números del 30 al 37, más ocho versiones brillantes del 90 al 97:

ColorNormalBrillante
Negro\033[30m\033[90m
Rojo\033[31m\033[91m
Verde\033[32m\033[92m
Amarillo\033[33m\033[93m
Azul\033[34m\033[94m
Magenta\033[35m\033[95m
Cian\033[36m\033[96m
Blanco\033[37m\033[97m

El tono exacto de cada uno lo decide el tema de la terminal, no tu programa. El "rojo" de quien use tu herramienta con un tema claro no será el mismo que el tuyo. Eso es bueno: respeta las preferencias de cada uno y garantiza que el texto se lea sobre su fondo.

Los colores de fondo#

Los mismos ocho, sumando 10 a cada número: del 40 al 47, y del 100 al 107 para los brillantes.

ColorFondoFondo brillante
Negro\033[40m\033[100m
Rojo\033[41m\033[101m
Verde\033[42m\033[102m
Amarillo\033[43m\033[103m
Azul\033[44m\033[104m
Magenta\033[45m\033[105m
Cian\033[46m\033[106m
Blanco\033[47m\033[107m

Los estilos#

Cinco que funcionan en casi cualquier terminal actual:

EstiloCódigoNotas
Negrita\033[1mEn algunas terminales solo aclara el color
Tenue\033[2mPara lo secundario. No lo soportan todas
Cursiva\033[3mBien soportada en macOS y Linux, irregular en Windows
Subrayado\033[4mMuy fiable
Invertido\033[7mIntercambia texto y fondo. El más fiable de todos

El invertido merece un comentario: como intercambia los colores que ya usa la terminal, siempre se lee, con tema claro o oscuro. Es la forma más segura de destacar una línea entera.

Combinar códigos#

Los parámetros se pueden encadenar separados por punto y coma, o poner uno detrás de otro. Es lo mismo:

python
print("\033[1;31mrojo y en negrita\033[0m")
print("\033[1m\033[31mrojo y en negrita\033[0m")

Un solo \033[0m al final cierra todo lo que se haya abierto, no hace falta uno por estilo.

256 colores y color real#

Los ocho colores básicos se quedan cortos en cuanto quieres matices. Hay dos ampliaciones, y la estructura del código cambia:

consola
>>> f"\033[38;5;208m"
'\x1b[38;5;208m'
>>> f"\033[38;2;255;128;0m"
'\x1b[38;2;255;128;0m'
FormaCódigoQué significa
Paleta de 256\033[38;5;NmEl color número N de una paleta fija, de 0 a 255
Color de 24 bits\033[38;2;R;G;BmEl color exacto, con sus tres componentes de 0 a 255

El 38 significa "color de texto ampliado" y el 48 es su equivalente para el fondo: \033[48;5;208m pinta el fondo de naranja. El 5 o el 2 de después indican cuál de las dos formas viene detrás.

En la paleta de 256, los números del 0 al 15 son los dieciséis colores básicos, del 16 al 231 hay un cubo de tonos y del 232 al 255, una escala de grises. Este programa la pinta entera:

paleta.py
for numero in range(256):
        print(f"\033[38;5;{numero}m{numero:>4}", end="")
    if numero % 16 == 15:
                print("\033[0m")

En pantalla salen dieciséis filas de dieciséis números, cada uno escrito de su color. Es la forma rápida de elegir un tono: lo miras y te quedas con el número.

El módulo colores.py#

Escribir \033[31m a mano por todo el programa es ilegible y frágil. Lo que se hace es meterlo en constantes, una vez, y no volver a mirarlo:

colores.py
"""Colores y estilos ANSI para la terminal, sin bibliotecas externas."""

import os
import sys

RESET = "\033[0m"

NEGRO = "\033[30m"
ROJO = "\033[31m"
VERDE = "\033[32m"
AMARILLO = "\033[33m"
AZUL = "\033[34m"
MAGENTA = "\033[35m"
CIAN = "\033[36m"
BLANCO = "\033[37m"

FONDO_ROJO = "\033[41m"
FONDO_VERDE = "\033[42m"
FONDO_AMARILLO = "\033[43m"
FONDO_AZUL = "\033[44m"

NEGRITA = "\033[1m"
TENUE = "\033[2m"
CURSIVA = "\033[3m"
SUBRAYADO = "\033[4m"
INVERTIDO = "\033[7m"


def hay_color() -> bool:
        """Decide si tiene sentido escribir códigos de color."""
    if os.environ.get("NO_COLOR"):
                return False
    if os.environ.get("TERM") == "dumb":
                return False
    return sys.stdout.isatty()


COLOR_ACTIVO = hay_color()


def pintar(texto: str, *codigos: str) -> str:
        """Devuelve el texto envuelto en los códigos dados, o tal cual sin color."""
    if not COLOR_ACTIVO or not codigos:
                return texto
    return "".join(codigos) + texto + RESET


def color_256(numero: int) -> str:
        """Código de texto para uno de los 256 colores de la paleta."""
    return f"\033[38;5;{numero}m"


def color_rgb(rojo: int, verde: int, azul: int) -> str:
        """Código de texto para un color exacto de 24 bits."""
    return f"\033[38;2;{rojo};{verde};{azul}m"

La función pintar recibe el texto y cuántos códigos quieras, gracias al *codigos de la lección de argumentos variables:

python
from colores import pintar, ROJO, VERDE, NEGRITA, SUBRAYADO, color_256

print(pintar("todo bien", VERDE))
print(pintar("fallo grave", ROJO, NEGRITA))
print(pintar("importante", SUBRAYADO))
print(pintar("naranja", color_256(208)))

Esa salida no se puede reproducir aquí: en la pantalla se ven cuatro líneas, la primera en verde, la segunda en rojo y negrita, la tercera subrayada y la cuarta naranja. El texto es exactamente el mismo que ves escrito; lo único que cambia es su aspecto.

Cuándo no hay que colorear#

Aquí está la parte que casi todo el mundo se salta, y es la que separa un programa amateur de uno serio. Los códigos de color son caracteres normales, así que si la salida no va a una terminal, se guardan en el fichero y lo ensucian. Esto es lo que aparecería dentro de un fichero al redirigir la salida de un programa que colorea sin comprobar nada:

text
^[[32m[OK]    Copiados 24 ficheros^[[0m

Ese ^[ es el carácter de escape visto por un editor de texto. Nadie quiere eso en un registro de actividad ni en un CSV.

La comprobación clave es sys.stdout.isatty(), que responde a la pregunta "¿mi salida es una terminal de verdad?". Devuelve False cuando el programa se ha lanzado con > a un fichero o con | hacia otro programa. Además de eso, hay dos convenciones que conviene respetar:

ComprobaciónPor qué
sys.stdout.isatty()Si no hay terminal, nadie va a ver los colores y solo estorban
La variable NO_COLORConvención aceptada por muchas herramientas: si existe, no se colorea
TERM con valor dumbTerminal sin capacidades, típica de algunos editores y entornos de integración continua

Con las tres metidas en hay_color(), el mismo programa se comporta bien en los dos casos. Este es el ejemplo de mensajes de estado:

estado.py
"""Mensajes de estado en verde, amarillo y rojo."""

import sys

from colores import pintar, ROJO, VERDE, AMARILLO, NEGRITA, COLOR_ACTIVO


def exito(mensaje: str) -> None:
        print(pintar(f"[OK]    {mensaje}", VERDE))


def aviso(mensaje: str) -> None:
        print(pintar(f"[AVISO] {mensaje}", AMARILLO))


def error(mensaje: str) -> None:
        print(pintar(f"[ERROR] {mensaje}", ROJO, NEGRITA), file=sys.stderr)


if __name__ == "__main__":
        print(f"¿Color activo? {COLOR_ACTIVO}")
    exito("Copiados 24 ficheros")
    aviso("El fichero de configuración está vacío")
    error("No existe la carpeta destino")

Ejecutado en una terminal, las tres líneas salen de su color. Ejecutado con la salida redirigida, isatty() devuelve False y el fichero queda limpio:

bash
python3 estado.py > registro.txt
cat registro.txt
Salida en pantalla
¿Color activo? False
[OK]    Copiados 24 ficheros
[AVISO] El fichero de configuración está vacío

La línea del error no está en el fichero porque salió por stderr, que no se ha redirigido: apareció en la pantalla, que es justo donde hace falta.

Lo mismo ocurre con la variable de entorno, sin tocar ni una línea del programa:

bash
NO_COLOR=1 python3 estado.py

Y ahí está la razón de fondo para que todo el color pase por pintar: la decisión de colorear o no se toma en un solo sitio. Con los códigos repartidos por el programa habría que añadir un if en cada print.

Windows#

Desde Windows 10, la terminal entiende los códigos ANSI igual que macOS y Linux, y todo lo de esta lección funciona tal cual. En las versiones antiguas, y en algunas consolas empotradas, los códigos aparecían escritos en la pantalla en vez de interpretarse.

La solución habitual, si necesitas dar soporte a eso, es la biblioteca colorama, que se instala con pip y traduce los códigos a llamadas de la API de Windows:

python
import colorama

colorama.just_fix_windows_console()

Esa línea al principio del programa no hace nada en macOS ni en Linux, y arregla las consolas antiguas de Windows. Es la única dependencia externa que merece la pena considerar para esto; el resto de la lección no necesita instalar nada.