Saltar al contenido
🐍 Curso de Python
/

Módulo 1 · Lección 50 de 59

Tipos de excepción, else y finally

Cómo capturar varias excepciones distintas, leer su mensaje, y qué hacen exactamente las cláusulas else y finally.

⏱ 12 min de lectura

Con un solo except ValueError cubres el caso más común, pero un trozo de código real puede fallar de varias formas a la vez. Convertir un texto a número y dividir puede dar ValueError o ZeroDivisionError, y cada uno merece un mensaje distinto. Y a veces necesitas algo más: ejecutar una cosa solo si todo fue bien, u otra pase lo que pase.

Las excepciones forman una familia#

Las excepciones de Python están organizadas en una jerarquía, como un árbol genealógico. ValueError y ZeroDivisionError no son cosas sueltas: descienden de tipos más generales, y ese parentesco tiene un efecto práctico muy concreto.

jerarquía de excepciones
Exception
    ArithmeticError
        ZeroDivisionError
    LookupError
        IndexError
        KeyError
    OSError
        FileNotFoundError
        PermissionError
    TypeError
    ValueError
    AttributeError

Capturar un tipo captura también a todos sus descendientes. Por eso esto funciona:

python
try:
        numeros = [10, 20, 30]
    print(numeros[7])
except LookupError as error:
        print(f"Capturado por LookupError: {error}")
Salida en pantalla
Capturado por LookupError: list index out of range

Lo que se ha lanzado es un IndexError, pero como desciende de LookupError, el except lo atrapa igual. Y en la cima está Exception, de la que desciende prácticamente todo:

python
try:
        int("hola")
except Exception as error:
        print(f"Capturado por Exception: {error}")
Salida en pantalla
Capturado por Exception: invalid literal for int() with base 10: 'hola'

No hace falta que te aprendas el árbol. Basta con que sepas que existe y que entiendas las dos consecuencias: capturar un tipo general es cómodo pero impreciso, y el orden de los except importa.

Varios except#

Un bloque try puede tener tantos except como quieras. Python los prueba de arriba abajo y ejecuta el primero que encaje, uno solo.

division.py
def dividir(texto_a: str, texto_b: str) -> None:
        try:
                resultado = int(texto_a) / int(texto_b)
    except ValueError:
                print("Alguno de los dos no era un número entero.")
    except ZeroDivisionError:
                print("No se puede dividir entre cero.")
    else:
                print(f"El resultado es {resultado}")


dividir("10", "4")
dividir("10", "0")
dividir("10", "dos")
Salida en pantalla
El resultado es 2.5
No se puede dividir entre cero.
Alguno de los dos no era un número entero.

Varias excepciones en un solo except#

Si vas a reaccionar igual ante varias, no repitas el bloque: agrúpalas en una tupla.

python
for valor in ["8", "0", "ocho"]:
        try:
                resultado = 100 / int(valor)
    except (ValueError, ZeroDivisionError) as error:
                print(f"No he podido dividir entre '{valor}': {error}")
    else:
                print(f"100 / {valor} = {resultado}")
Salida en pantalla
100 / 8 = 12.5
No he podido dividir entre '0': division by zero
No he podido dividir entre 'ocho': invalid literal for int() with base 10: 'ocho'

Los paréntesis son obligatorios: except (ValueError, ZeroDivisionError), no except ValueError, ZeroDivisionError.

Capturar el objeto con as#

as error guarda la excepción en una variable para que puedas mirarla. Lo que más se usa es imprimirla, porque al convertirla a texto da su mensaje:

python
try:
        numero = int("treinta y dos")
except ValueError as error:
        print(error)
    print(type(error))
Salida en pantalla
invalid literal for int() with base 10: 'treinta y dos'
<class 'ValueError'>

El mensaje viene en inglés, claro, porque lo escribe Python. Lo interesante es que trae el dato concreto que falló: 'treinta y dos'. Eso convierte un aviso genérico en uno útil.

python
datos = {"nombre": "Lucía", "ciudad": "Gijón"}

for clave in ["nombre", "edad"]:
        try:
                print(datos[clave])
    except KeyError as error:
                print(f"Falta la clave {error} en los datos")
Salida en pantalla
Lucía
Falta la clave 'edad' en los datos

Las cláusulas else y finally#

Un bloque completo puede tener cuatro partes. Este ejemplo enseña cuándo se ejecuta cada una:

cuatro-partes.py
def procesar(texto: str) -> None:
        print(f"--- procesando '{texto}' ---")
    try:
                numero = int(texto)
    except ValueError as error:
                print(f"except: {error}")
    else:
                print(f"else: la conversión ha ido bien, vale {numero}")
    finally:
                print("finally: esto se ejecuta siempre")


procesar("42")
procesar("cuarenta y dos")
Salida en pantalla
--- procesando '42' ---
else: la conversión ha ido bien, vale 42
finally: esto se ejecuta siempre
--- procesando 'cuarenta y dos' ---
except: invalid literal for int() with base 10: 'cuarenta y dos'
finally: esto se ejecuta siempre
BloqueCuándo se ejecuta
trySiempre, hasta que algo falle
exceptSolo si se lanzó una excepción de ese tipo
elseSolo si no se lanzó ninguna excepción
finallySiempre, haya fallado o no

Para qué sirve else#

Podrías meter el código del else al final del try y aparentemente daría igual. La diferencia es de precisión: lo que va en el try queda vigilado por el except, y lo que va en el else, no. Si el mensaje del else lanzara por su cuenta un ValueError, con el código dentro del try acabaría capturado por error, disfrazado de fallo de conversión.

else dice exactamente lo que quieres decir: "vigila solo esta línea, y si sale bien, haz esto otro".

Para qué sirve finally#

finally es para la limpieza: cerrar, liberar, desconectar. Su garantía es fuerte, porque se ejecuta incluso si el except no captura la excepción y el programa se va a morir, e incluso si dentro del try hay un return.

python
def leer_primera_linea(ruta: str) -> str:
        fichero = open(ruta, encoding="utf-8")
    try:
                return fichero.readline().strip()
    finally:
                fichero.close()

Ese return devuelve el valor, pero antes de salir de verdad de la función se ejecuta el finally y el fichero se cierra. Por cierto: eso es exactamente lo que hace with por dentro, y por eso con with no necesitas escribir este finally a mano.

Las excepciones que más vas a ver#

ExcepciónQué la provocaEjemplo que la lanza
ValueErrorEl tipo es correcto pero el valor no sirveint("hola")
TypeErrorLa operación no vale para ese tipo"3" + 3
KeyErrorLa clave no está en el diccionarioagenda["Mateo"]
IndexErrorLa posición está fuera de la listanotas[3] con 3 elementos
FileNotFoundErrorLa ruta no existeopen("no-existe.txt")
ZeroDivisionErrorDividir o sacar el resto entre cero10 / 0
AttributeErrorEse método o atributo no existe en ese tipo"hola".longitud()

Los mensajes exactos que imprimen son estos, y conviene reconocerlos de un vistazo:

Salida en pantalla
ValueError: invalid literal for int() with base 10: 'hola'
TypeError: can only concatenate str (not "int") to str
KeyError: 'Mateo'
IndexError: list index out of range
FileNotFoundError: [Errno 2] No such file or directory: 'no-existe.txt'
ZeroDivisionError: division by zero
AttributeError: 'str' object has no attribute 'longitud'

El antipatrón: except Exception y pass#

python
# no hagas esto
try:
        procesar_pedidos()
except Exception:
        pass

Esas cuatro líneas parecen hacer el programa robusto. Lo que hacen es volverlo imposible de arreglar. Si algo falla, no hay mensaje, no hay traceback, no hay registro: el programa sigue como si nada con los datos a medias, y el día que alguien note que faltan pedidos no habrá ni una pista de cuándo empezó ni por qué.

Si de verdad necesitas que un fallo no detenga el programa —por ejemplo, procesando mil ficheros de los que uno puede estar corrupto—, captura y deja constancia:

python
for fichero in ficheros:
        try:
                procesar(fichero)
    except Exception as error:
                print(f"No se ha podido procesar {fichero}: {error}")

Ahora el proceso continúa y al terminar sabes exactamente qué falló y con qué mensaje. La diferencia entre las dos versiones son quince caracteres y varias horas de tu tiempo futuro.