Excepciones en Python
En esta lección
Todo programa real se encuentra con situaciones imprevistas: el usuario escribe “veinte” donde esperabas un número, un fichero no existe, una lista llega vacía. En Python, cuando algo así ocurre se lanza una excepción, y si nadie la gestiona, el programa se detiene. En esta lección aprenderás a leer los mensajes de error, a capturar excepciones con try y except para que tu programa reaccione con cabeza, y a lanzar las tuyas propias.
Leer un traceback
Antes de capturar errores hay que saber leerlos. Este programa calcula el precio medio de una tienda que todavía no tiene productos:
def precio_medio(precios):
return sum(precios) / len(precios)
def informe(tienda):
media = precio_medio(tienda["precios"])
print(f"Precio medio: {media}")
informe({"nombre": "Centro", "precios": []})
Al ejecutarlo aparece esto:
Traceback (most recent call last):
File "tienda.py", line 8, in <module>
informe({"nombre": "Centro", "precios": []})
File "tienda.py", line 5, in informe
media = precio_medio(tienda["precios"])
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "tienda.py", line 2, in precio_medio
return sum(precios) / len(precios)
~~~~~~~~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero
Un traceback es el camino de llamadas que llevó al error. Se lee así:
- Empieza por la última línea: el tipo de excepción (
ZeroDivisionError) y un mensaje (division by zero). Es lo más importante. - Sube una línea: el archivo, la línea (
line 2) y la función (precio_medio) donde saltó. Los símbolos^y~señalan la operación exacta. - Sigue subiendo para ver quién llamó a quién:
<module>(el programa principal) llamó ainforme, que llamó aprecio_medio.
“Most recent call last” significa que la llamada más reciente está abajo. Casi siempre el fallo está en la última línea de tu código que aparece en el traceback.
Excepciones más comunes
| Excepción | Cuándo ocurre | Ejemplo |
|---|---|---|
ValueError | El tipo es correcto pero el valor no sirve | int("hola") |
TypeError | Operación con un tipo que no corresponde | "Edad: " + 25 |
KeyError | La clave no existe en el diccionario | {"a": 1}["b"] |
IndexError | La posición no existe en la lista | [1, 2][5] |
ZeroDivisionError | División entre cero | 10 / 0 |
FileNotFoundError | El fichero no existe | open("no_existe.txt") |
NameError | Usas una variable que no está definida | print(nombre) |
AttributeError | El objeto no tiene ese método o atributo | "hola".push("x") |
Todas son clases que heredan de Exception. Aprenderte esta tabla te ahorrará mucho tiempo: el nombre del error ya te dice qué buscar.
try y except
El código que puede fallar se pone en un bloque try. Si se lanza una excepción, Python salta al bloque except correspondiente y el programa continúa:
def pedir(texto):
try:
n = int(texto)
print(f"Tienes {n} años")
except ValueError:
print("Eso no es un número entero")
pedir("30") # Tienes 30 años
pedir("treinta") # Eso no es un número entero
En cuanto una línea del try falla, las siguientes no se ejecutan: por eso el segundo print del try no aparece con “treinta”.
Varios except
Puedes tratar cada tipo de error de forma distinta. Python prueba los except en orden y usa el primero que encaja:
def dividir(a, b):
try:
return a / b
except ZeroDivisionError:
print("No se puede dividir entre cero")
except TypeError:
print("Los dos valores deben ser números")
return None
print(dividir(10, 2)) # 5.0
print(dividir(10, 0)) # No se puede dividir entre cero / None
print(dividir(10, "2")) # Los dos valores deben ser números / None
Si varias excepciones se tratan igual, agrúpalas en una tupla:
def a_numero(valor):
try:
return float(valor)
except (ValueError, TypeError):
return 0.0
print(a_numero("3.5"), a_numero("abc"), a_numero(None)) # 3.5 0.0 0.0
Acceder al error con as
Con as guardas la excepción en una variable para mostrar su mensaje o registrarlo:
try:
int("doce")
except ValueError as e:
print("Error:", e) # Error: invalid literal for int() with base 10: 'doce'
print(type(e).__name__) # ValueError
else y finally
El bloque try tiene dos partes opcionales más:
else: se ejecuta solo si no hubo excepción.finally: se ejecuta siempre, haya error o no.
def leer_nota(texto):
try:
nota = float(texto)
except ValueError:
print(f"'{texto}' no es una nota válida")
else:
print(f"Nota guardada: {nota}")
finally:
print("Fin de la lectura")
leer_nota("7.5")
# Nota guardada: 7.5
# Fin de la lectura
leer_nota("siete")
# 'siete' no es una nota válida
# Fin de la lectura
¿Por qué no meter el print de else dentro del try? Porque conviene que el try contenga solo la línea que puede fallar. Si metes más código, podrías capturar por accidente un error de otra línea que no esperabas.
finally sirve para tareas de limpieza que deben hacerse sí o sí: cerrar una conexión, guardar un registro, liberar un recurso. Se ejecuta incluso si hay un return dentro del try:
def prueba():
try:
return "desde try"
finally:
print("finally se ejecuta igualmente")
print(prueba())
# finally se ejecuta igualmente
# desde try
Con ficheros normalmente no necesitarás finally, porque with cierra el fichero solo (lo verás en ficheros).
Lanzar excepciones con raise
Tus funciones también pueden avisar de que algo va mal. En lugar de devolver un valor raro como -1 o None, lanza una excepción con raise:
def retirar(saldo, cantidad):
if cantidad <= 0:
raise ValueError("La cantidad debe ser positiva")
if cantidad > saldo:
raise ValueError(f"Saldo insuficiente: tienes {saldo} €")
return saldo - cantidad
print(retirar(100, 30)) # 70
try:
retirar(100, 500)
except ValueError as e:
print("No se pudo retirar:", e)
# No se pudo retirar: Saldo insuficiente: tienes 100 €
La ventaja es que el error no se puede ignorar sin querer: o alguien lo captura, o el programa se detiene con un mensaje claro.
Dentro de un except, un raise sin nada más vuelve a lanzar la misma excepción. Es útil si quieres registrar el error pero dejar que otro lo gestione.
Excepciones propias
Para errores de tu propio dominio (una tienda, un banco, un gimnasio) puedes crear tus excepciones heredando de Exception. Basta con una clase vacía con un docstring (las clases las verás más adelante):
class SaldoInsuficienteError(Exception):
"""Se lanza cuando no hay dinero suficiente en la cuenta."""
class CuentaBloqueadaError(Exception):
"""Se lanza al operar con una cuenta bloqueada."""
def retirar(saldo, cantidad, bloqueada=False):
if bloqueada:
raise CuentaBloqueadaError("La cuenta está bloqueada")
if cantidad > saldo:
raise SaldoInsuficienteError(f"Faltan {cantidad - saldo} €")
return saldo - cantidad
try:
retirar(50, 80)
except SaldoInsuficienteError as e:
print("Saldo:", e) # Saldo: Faltan 30 €
except CuentaBloqueadaError as e:
print("Bloqueo:", e)
Así quien use tu código puede distinguir cada situación y actuar en consecuencia. Por convención, sus nombres terminan en Error.
Si creas una excepción base para tu aplicación y haces que las demás hereden de ella, podrás capturarlas todas de golpe:
class ErrorTienda(Exception): pass
class SinStockError(ErrorTienda): pass
class CuponInvalidoError(ErrorTienda): pass
try:
raise SinStockError("No quedan camisetas")
except ErrorTienda as e:
print(type(e).__name__, e) # SinStockError No quedan camisetas
Validar lo que escribe el usuario
Uno de los usos más habituales: pedir un dato con input hasta que sea válido. Se combina un bucle while True con try:
def pedir_edad():
while True:
texto = input("¿Cuántos años tienes? ")
try:
edad = int(texto)
except ValueError:
print("Escribe un número entero, por ejemplo 25.")
continue
if edad < 0 or edad > 120:
print("La edad debe estar entre 0 y 120.")
continue
return edad
edad = pedir_edad()
print(f"Edad registrada: {edad}")
¿Cuántos años tienes? veinte
Escribe un número entero, por ejemplo 25.
¿Cuántos años tienes? -5
La edad debe estar entre 0 y 120.
¿Cuántos años tienes? 20
Edad registrada: 20
continue vuelve al principio del bucle y return sale de la función con el valor bueno.
EAFP frente a LBYL
Hay dos estilos para tratar situaciones que pueden fallar:
- LBYL (Look Before You Leap, “mira antes de saltar”): compruebas antes con un
if. - EAFP (Easier to Ask Forgiveness than Permission, “es más fácil pedir perdón que permiso”): lo intentas y capturas el error si falla.
precios = {"pan": 1.2}
# LBYL
if "leche" in precios:
precio = precios["leche"]
else:
precio = 0
# EAFP
try:
precio = precios["leche"]
except KeyError:
precio = 0
En Python se considera más natural el estilo EAFP, sobre todo cuando comprobar antes es difícil o poco fiable (por ejemplo, si un fichero existe: podría borrarse entre la comprobación y la apertura). Aun así, usa el sentido común: si hay una forma directa, como precios.get("leche", 0), es mejor que ambas.
Errores frecuentes
No uses except desnudo
try:
total = calcular()
except: # mal
print("Algo ha fallado")
Un except: sin tipo captura absolutamente todo, incluidos los errores de escritura de tu propio código (un NameError por una errata) y hasta Ctrl+C. El programa sigue como si nada y nunca sabrás qué ha pasado. Captura siempre el tipo concreto que esperas. Si de verdad necesitas algo general, usa except Exception as e: y muestra o registra e.
Otros descuidos
- Silenciar errores con
pass:except ValueError: passesconde el problema. Al menos informa de él. - Meter demasiado código en el
try: captura solo lo que puede fallar. - Orden incorrecto de los
except: si ponesexcept Exceptionprimero, los más concretos de debajo nunca se ejecutan. - Usar excepciones como
if: si una simple comparación resuelve el caso, no hace faltatry.
assert, en pocas palabras
assert comprueba algo que debería ser siempre cierto y lanza AssertionError si no lo es:
def media(notas):
assert len(notas) > 0, "La lista de notas está vacía"
return sum(notas) / len(notas)
media([]) # AssertionError: La lista de notas está vacía
Úsalo para detectar errores de programación mientras desarrollas y en los tests. No lo uses para validar datos del usuario: Python puede ejecutarse con la opción -O, que elimina todos los assert. Para validar, usa if y raise.
Resumen
| Elemento | Para qué sirve |
|---|---|
| Traceback | Muestra el tipo de error, el mensaje y el camino de llamadas; se lee de abajo arriba |
try | Contiene el código que puede fallar |
except Tipo | Gestiona un tipo concreto de error |
except (A, B) | Gestiona varios tipos igual |
as e | Guarda la excepción para ver su mensaje |
else | Se ejecuta si no hubo error |
finally | Se ejecuta siempre |
raise | Lanza una excepción |
class MiError(Exception) | Crea una excepción propia |
assert | Comprueba condiciones internas mientras desarrollas |
Regla de oro: captura solo lo que sabes tratar y deja que lo demás salte con un mensaje claro. En la siguiente lección aprenderás a organizar tu código en módulos.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.