Módulo 2 · Lección 14 de 28
Códigos de salida y stderr
Cómo decirle al sistema si tu programa ha ido bien o mal, y por qué los errores no se imprimen por la salida normal.
Hasta ahora tus programas se ejecutaban y ya está. Los mirabas tú, veías si funcionaban y listo. Pero una herramienta de terminal casi nunca vive sola: se llama desde otro script, desde una tarea programada, desde un sistema de integración continua o desde la propia terminal encadenada con otros comandos. Y ninguno de esos sitios va a leer tu mensaje de error: son máquinas.
Para que una máquina sepa si tu programa fue bien, existe un canal aparte del texto que imprimes. Al terminar, todo programa devuelve un número entero al sistema operativo. Ese número es el código de salida, y es la única forma que tiene el mundo exterior de saber qué ha pasado ahí dentro.
Cero es éxito, lo demás es fallo#
La convención es universal y se cumple en Linux, en macOS y en Windows:
- 0 significa que todo fue bien.
- Cualquier otro número significa que algo falló.
Es al revés de lo que dice la intuición, y tiene su lógica: hay una única forma de que algo salga bien, pero muchas formas distintas de que salga mal. El cero se reserva para el éxito y quedan 255 números libres para distinguir los fracasos.
Consultarlo en la terminal#
La terminal guarda el código del último comando en la variable $?. Puedes verlo con echo:
ls datos.txt
echo $?datos.txt
0Y ahora con un fichero que no existe:
ls no-existe.txt
echo $?ls: no-existe.txt: No such file or directory
1ls no solo ha escrito un mensaje: ha devuelto un 1. Ese 1 es lo que puede leer un script.
Devolverlo desde Python#
Si tu programa termina normalmente, Python devuelve 0 por su cuenta. Si muere con una excepción sin capturar, devuelve 1 y escribe la traza. Para cualquier otra cosa, lo eliges tú.
La forma directa es sys.exit():
import sys
if not hay_configuracion():
sys.exit(1)sys.exit() corta la ejecución en el acto, esté donde esté, aunque sea dentro de una función anidada a tres niveles. Admite tres cosas:
| Llamada | Código devuelto | Qué imprime |
|---|---|---|
sys.exit() | 0 | Nada |
sys.exit(2) | 2 | Nada |
sys.exit("mensaje") | 1 | El mensaje, por stderr |
Esa tercera forma es un atajo cómodo: si le pasas un texto en vez de un número, Python lo imprime como error y usa el código 1.
import sys
print("Empiezo")
sys.exit("Falta el fichero de configuración")
print("Esto no se ejecuta nunca")python3 salir.py
echo $?Empiezo
Falta el fichero de configuración
1sys.exit es en realidad una excepción#
Por dentro, sys.exit() no hace magia: lanza una excepción llamada SystemExit. Python la deja subir hasta arriba del todo y, al llegar, usa su valor como código de salida. Por eso estas dos líneas son equivalentes:
sys.exit(2)
raise SystemExit(2)La segunda tiene la ventaja de no necesitar import sys y de dejar clarísimo que lo que ocurre es una excepción que sube. Úsala cuando te convenga; no hay ninguna diferencia de comportamiento.
El patrón limpio: main devuelve el código#
Sembrar sys.exit() por todo el programa funciona, pero hace el código difícil de seguir y casi imposible de probar: cualquier función puede matar el proceso. El patrón que usan las herramientas serias es otro, y consiste en separar dos responsabilidades.
main() es una función normal que devuelve un número entero. Y el arranque del módulo se encarga de convertir ese número en la salida real del proceso:
import sys
from pathlib import Path
def main() -> int:
if len(sys.argv) != 2:
print("Uso: comprobar.py RUTA", file=sys.stderr)
return 2
ruta = Path(sys.argv[1])
if not ruta.exists():
print(f"No existe el fichero: {ruta}", file=sys.stderr)
return 1
print(f"{ruta} ocupa {ruta.stat().st_size} bytes")
return 0
raise SystemExit(main())Los tres casos, con un datos.txt creado antes con echo "hola" > datos.txt:
python3 comprobar.py datos.txt
echo $?datos.txt ocupa 5 bytes
0python3 comprobar.py otro.txt
echo $?No existe el fichero: otro.txt
1python3 comprobar.py
echo $?Uso: comprobar.py RUTA
2Lo que hace que el patrón merezca la pena es que main() sigue siendo una función corriente. Puedes llamarla desde otro programa, comprobar que devuelve 2 cuando faltan argumentos y seguir trabajando, porque no ha matado nada. La decisión de terminar el proceso vive en una sola línea, la última, y se ve de un vistazo.
Los dos flujos de salida#
Fíjate en una cosa del programa anterior: los mensajes de error llevan file=sys.stderr. No es un adorno.
Todo programa nace con dos canales de salida separados:
| Flujo | Nombre completo | Para qué |
|---|---|---|
stdout | Salida estándar | El resultado del programa: los datos que produce |
stderr | Salida de error estándar | Errores, avisos, mensajes de progreso, cualquier cosa dirigida a la persona |
Los dos acaban en la misma pantalla, así que en el día a día parecen lo mismo. Dejan de parecerlo en cuanto alguien redirige la salida a un fichero o la encadena con otro programa.
Escribir en stderr#
print() escribe en stdout por defecto. Para mandar algo por el otro canal, se usa el parámetro file:
import sys
print("Esto es el resultado del programa")
print("Esto es un aviso para la persona", file=sys.stderr)
print("Y esto más resultado")Ejecutado sin más, no notas nada raro:
python3 dos_flujos.pyEsto es el resultado del programa
Esto es un aviso para la persona
Y esto más resultadoLa diferencia se ve al redirigir#
La terminal permite desviar cada flujo a un sitio distinto. > redirige stdout y 2> redirige stderr. Ese 2 es el número interno del flujo de error, igual que el 1 es el de la salida normal.
Mandamos el resultado a un fichero:
python3 dos_flujos.py > resultado.txtEsto es un aviso para la personaEn pantalla se queda solo el aviso, porque stderr no se ha redirigido. Y en el fichero ha caído justo lo contrario:
cat resultado.txtEsto es el resultado del programa
Y esto más resultadoAhora al revés, guardamos los errores:
python3 dos_flujos.py 2> errores.txtEsto es el resultado del programa
Y esto más resultadocat errores.txtEsto es un aviso para la personaEso es exactamente lo que se quiere: el fichero resultado.txt contiene datos limpios, sin una sola línea de conversación colada en medio. Si el programa hubiera escrito el aviso por stdout, ahora el fichero estaría contaminado y quien lo procesara se encontraría una línea inesperada.
Las combinaciones que más se usan:
| Comando | Qué hace |
|---|---|
programa > salida.txt | Guarda el resultado, los errores se ven en pantalla |
programa 2> errores.txt | Guarda los errores, el resultado se ve en pantalla |
programa > salida.txt 2> errores.txt | Cada cosa a su fichero |
programa > todo.txt 2>&1 | Los dos al mismo fichero |
programa 2> /dev/null | Tira los errores a la basura |
programa > /dev/null | Tira el resultado y deja ver solo los errores |
/dev/null es un fichero especial de Linux y macOS que se traga todo lo que le escribas. En Windows el equivalente es NUL.
Encadenar programas según el resultado#
Aquí es donde el código de salida deja de ser teoría. La terminal tiene dos operadores que miran ese número y deciden si sigue o no:
A && BejecutaBsolo siAfue bien (devolvió 0).A || BejecutaBsolo siAfalló (devolvió algo distinto de 0).
python3 comprobar.py datos.txt && echo "Sigo adelante"datos.txt ocupa 5 bytes
Sigo adelantepython3 comprobar.py otro.txt && echo "Sigo adelante"No existe el fichero: otro.txtEl echo no se ha ejecutado. Y con ||, al revés:
python3 comprobar.py otro.txt || echo "Aborto el proceso"No existe el fichero: otro.txt
Aborto el procesoEsto es lo que hay detrás de una línea como python3 descargar.py && python3 procesar.py && python3 enviar.py: tres programas en cadena donde el fallo de cualquiera detiene el resto. Y funciona sin que ninguno de los tres sepa nada de los otros, solo porque todos respetan la convención del cero.
Códigos de salida convencionales#
Con 0 y 1 se cubre casi todo. Estos son los que tienen un significado acordado y conviene respetar:
| Código | Significado |
|---|---|
0 | Éxito |
1 | Error genérico: algo no ha ido bien |
2 | Uso incorrecto: faltan argumentos o son inválidos |
126 | El fichero existe pero no se puede ejecutar |
127 | El comando no existe |
130 | Interrumpido por el usuario con Ctrl + C |
137 | El proceso fue matado por el sistema, normalmente por falta de memoria |
Los códigos del 3 al 125 son tuyos: puedes darles el significado que quieras en tu herramienta, siempre que lo documentes en la ayuda. Y evita pasarte de 255, porque el sistema solo guarda el último byte: sys.exit(256) acaba devolviendo 0, con lo que un error se convertiría en un éxito.