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.
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.
Exception
ArithmeticError
ZeroDivisionError
LookupError
IndexError
KeyError
OSError
FileNotFoundError
PermissionError
TypeError
ValueError
AttributeErrorCapturar un tipo captura también a todos sus descendientes. Por eso esto funciona:
try:
numeros = [10, 20, 30]
print(numeros[7])
except LookupError as error:
print(f"Capturado por LookupError: {error}")Capturado por LookupError: list index out of rangeLo 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:
try:
int("hola")
except Exception as error:
print(f"Capturado por Exception: {error}")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.
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")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.
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}")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:
try:
numero = int("treinta y dos")
except ValueError as error:
print(error)
print(type(error))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.
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")Lucía
Falta la clave 'edad' en los datosLas cláusulas else y finally#
Un bloque completo puede tener cuatro partes. Este ejemplo enseña cuándo se ejecuta cada una:
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")--- 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| Bloque | Cuándo se ejecuta |
|---|---|
try | Siempre, hasta que algo falle |
except | Solo si se lanzó una excepción de ese tipo |
else | Solo si no se lanzó ninguna excepción |
finally | Siempre, 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.
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ón | Qué la provoca | Ejemplo que la lanza |
|---|---|---|
ValueError | El tipo es correcto pero el valor no sirve | int("hola") |
TypeError | La operación no vale para ese tipo | "3" + 3 |
KeyError | La clave no está en el diccionario | agenda["Mateo"] |
IndexError | La posición está fuera de la lista | notas[3] con 3 elementos |
FileNotFoundError | La ruta no existe | open("no-existe.txt") |
ZeroDivisionError | Dividir o sacar el resto entre cero | 10 / 0 |
AttributeError | Ese método o atributo no existe en ese tipo | "hola".longitud() |
Los mensajes exactos que imprimen son estos, y conviene reconocerlos de un vistazo:
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#
# no hagas esto
try:
procesar_pedidos()
except Exception:
passEsas 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:
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.