Saltar al contenido
🐍 Curso de Python
/

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.

⏱ 19 min de lectura

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:

bash
ls datos.txt
echo $?
Salida en pantalla
datos.txt
0

Y ahora con un fichero que no existe:

bash
ls no-existe.txt
echo $?
Salida en pantalla
ls: no-existe.txt: No such file or directory
1

ls 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():

python
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:

LlamadaCódigo devueltoQué imprime
sys.exit()0Nada
sys.exit(2)2Nada
sys.exit("mensaje")1El 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.

salir.py
import sys

print("Empiezo")
sys.exit("Falta el fichero de configuración")
print("Esto no se ejecuta nunca")
bash
python3 salir.py
echo $?
Salida en pantalla
Empiezo
Falta el fichero de configuración
1

sys.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:

python
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:

comprobar.py
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:

bash
python3 comprobar.py datos.txt
echo $?
Salida en pantalla
datos.txt ocupa 5 bytes
0
bash
python3 comprobar.py otro.txt
echo $?
Salida en pantalla
No existe el fichero: otro.txt
1
bash
python3 comprobar.py
echo $?
Salida en pantalla
Uso: comprobar.py RUTA
2

Lo 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:

FlujoNombre completoPara qué
stdoutSalida estándarEl resultado del programa: los datos que produce
stderrSalida de error estándarErrores, 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:

dos_flujos.py
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:

bash
python3 dos_flujos.py
Salida en pantalla
Esto es el resultado del programa
Esto es un aviso para la persona
Y esto más resultado

La 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:

bash
python3 dos_flujos.py > resultado.txt
Salida en pantalla
Esto es un aviso para la persona

En pantalla se queda solo el aviso, porque stderr no se ha redirigido. Y en el fichero ha caído justo lo contrario:

bash
cat resultado.txt
Salida en pantalla
Esto es el resultado del programa
Y esto más resultado

Ahora al revés, guardamos los errores:

bash
python3 dos_flujos.py 2> errores.txt
Salida en pantalla
Esto es el resultado del programa
Y esto más resultado
bash
cat errores.txt
Salida en pantalla
Esto es un aviso para la persona

Eso 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:

ComandoQué hace
programa > salida.txtGuarda el resultado, los errores se ven en pantalla
programa 2> errores.txtGuarda los errores, el resultado se ve en pantalla
programa > salida.txt 2> errores.txtCada cosa a su fichero
programa > todo.txt 2>&1Los dos al mismo fichero
programa 2> /dev/nullTira los errores a la basura
programa > /dev/nullTira 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 && B ejecuta B solo si A fue bien (devolvió 0).
  • A || B ejecuta B solo si A falló (devolvió algo distinto de 0).
bash
python3 comprobar.py datos.txt && echo "Sigo adelante"
Salida en pantalla
datos.txt ocupa 5 bytes
Sigo adelante
bash
python3 comprobar.py otro.txt && echo "Sigo adelante"
Salida en pantalla
No existe el fichero: otro.txt

El echo no se ha ejecutado. Y con ||, al revés:

bash
python3 comprobar.py otro.txt || echo "Aborto el proceso"
Salida en pantalla
No existe el fichero: otro.txt
Aborto el proceso

Esto 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ódigoSignificado
0Éxito
1Error genérico: algo no ha ido bien
2Uso incorrecto: faltan argumentos o son inválidos
126El fichero existe pero no se puede ejecutar
127El comando no existe
130Interrumpido por el usuario con Ctrl + C
137El 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.

Errores frecuentes#