Excepciones
A lo largo del manual ya te has cruzado con varias excepciones: TypeError al intentar sumar una cadena con un entero, ValueError al convertir "hola" a número, IndexError al pedir un elemento de una lista que no existe, FileNotFoundError al abrir un archivo que no está. Esta lección trata de qué son exactamente y cómo manejarlas sin que el programa se caiga.
Qué es una excepción
Una excepción es un evento que interrumpe el flujo normal del programa cuando algo va mal: una operación imposible, datos inválidos, un recurso que no está disponible. Si no se gestiona, Python detiene el programa y muestra un traceback: un informe de dónde ocurrió el problema:
>>> edad = int("veinte")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int() with base 10: 'veinte'
El traceback se lee de arriba abajo: la línea donde estabas, y al final el tipo de excepción (ValueError) con un mensaje explicando qué pasó. Aprender a leer tracebacks es de las habilidades que más rinde al principio: casi siempre te están diciendo exactamente lo que está mal.
Capturar excepciones con try/except
Para que el programa pueda reaccionar a un error en lugar de pararse, se envuelve el código sospechoso en un bloque try y se indica qué hacer si falla en un bloque except:
>>> entrada = "veinte"
>>> try:
... edad = int(entrada)
... print("la edad es", edad)
... except ValueError:
... print("eso no es un número válido")
eso no es un número válido
Si el código dentro del try se ejecuta sin problemas, el bloque except se ignora. Si lanza una excepción del tipo indicado, Python salta inmediatamente al except correspondiente y sigue desde ahí. El programa continúa, no se cae.
Varios except para excepciones distintas
Un mismo try puede ir seguido de varios except, cada uno para un tipo de error distinto:
try:
numero = int(input("escribe un número: "))
resultado = 100 / numero
print("100 dividido entre", numero, "es", resultado)
except ValueError:
print("eso no era un número")
except ZeroDivisionError:
print("no se puede dividir entre cero")
Python prueba los except en orden y ejecuta el primero cuyo tipo coincida con la excepción lanzada. Si no coincide ninguno, la excepción se propaga hacia fuera (y, si nadie la captura, acaba en un traceback).
Si varias excepciones se manejan igual, puedes agruparlas en una tupla:
try:
resultado = 100 / int(entrada)
except (ValueError, ZeroDivisionError):
print("entrada no válida")
Acceder al objeto de la excepción
A veces necesitas el mensaje exacto del error, por ejemplo para registrarlo en un log. Usa as para capturar el objeto de la excepción en una variable:
>>> try:
... int("veinte")
... except ValueError as e:
... print("error capturado:", e)
error capturado: invalid literal for int() with base 10: 'veinte'
La variable e es el objeto excepción completo: imprimirlo muestra el mensaje, pero también tiene atributos y se puede inspeccionar con type(e).
except Exception: el cajón de sastre
Si quieres capturar cualquier error que pueda ocurrir, usa except Exception. Casi todas las excepciones que puedes encontrarte heredan de la clase Exception, así que ese except las cubre todas:
try:
operacion_arriesgada()
except Exception as e:
print("algo ha ido mal:", e)
Nota: captura excepciones específicas siempre que puedas. Un except Exception genérico esconde bugs reales: el día que tengas un error de tipo distinto, lo capturarás silenciosamente y no te enterarás. Úsalo solo en el "borde" del programa (un bucle principal que no debe morir, por ejemplo), nunca como sustituto de pensar qué errores concretos esperas.
Hay también un except sin tipo (except: a secas) que captura literalmente todo, incluidas señales como Ctrl+C. No lo uses nunca.
Las cláusulas else y finally
Además de try y except, hay dos cláusulas más:
else: se ejecuta solo si no hubo excepción. Sirve para separar el "happy path" del manejo de error.finally: se ejecuta siempre, hubo error o no. Sirve para tareas de limpieza que tienen que pasar pase lo que pase.
try:
f = open("datos.txt")
contenido = f.read()
except FileNotFoundError:
print("el archivo no existe")
else:
print("archivo leído correctamente")
print(contenido[:50])
finally:
print("este mensaje se imprime siempre")
Nota: la cláusula finally se usaba antes para cerrar archivos pase lo que pase. Hoy en día eso lo resuelve la construcción with de manera mucho más limpia. Sigue siendo útil en otros contextos: cerrar conexiones de red, liberar bloqueos, restaurar estado.
Lanzar excepciones con raise
Hasta ahora hemos hablado de capturar excepciones que Python lanza por su cuenta. Pero tú también puedes lanzar tus propias excepciones con raise, útil cuando detectas algo inválido y quieres detener el flujo en ese punto:
def aplicar_descuento(precio, porcentaje):
if porcentaje < 0 or porcentaje > 100:
raise ValueError(f"porcentaje fuera de rango: {porcentaje}")
return precio * (1 - porcentaje / 100)
>>> aplicar_descuento(50, 120)
ValueError: porcentaje fuera de rango: 120
Lanzar una excepción es preferible a "devolver None si algo va mal": deja claro al que llame que la condición de error es realmente excepcional, e impide que el resto del código siga ejecutándose con datos inválidos.
Las excepciones más usadas para esto son ValueError (valor del tipo correcto pero contenido inválido), TypeError (tipo incorrecto) y RuntimeError (cajón genérico).
Relanzar después de actuar
Una variante útil de raise es la que aparece dentro de un except: capturas el error, haces algo con él (registrarlo, avisar al usuario), y lo relanzas para que siga propagándose:
try:
procesar_archivo("datos.csv")
except Exception as e:
print(f"error procesando datos.csv: {e}")
raise # vuelve a lanzar la misma excepción
El raise a secas (sin argumento), dentro de un except, relanza la excepción actual. Es el patrón cuando quieres dejar constancia del error pero no quieres tragártelo.
Excepciones más comunes
No tienes que memorizarlas, pero saber qué significan ayuda a interpretar tracebacks de un vistazo:
| Excepción | Cuándo aparece |
|---|---|
SyntaxError | El código no es Python válido. No se llega ni a ejecutar. |
IndentationError | Variante de SyntaxError por indentación incorrecta. |
NameError | Usas un nombre que no está definido (variable mal escrita, falta un import). |
TypeError | Operación con tipos incompatibles ("hola" + 5) o argumentos incorrectos. |
ValueError | El tipo está bien pero el valor no es válido (int("hola")). |
IndexError | Índice fuera de rango en una lista o tupla. |
KeyError | Clave que no existe en un diccionario. |
AttributeError | Atributo o método inexistente (None.upper()). |
ZeroDivisionError | División entre cero. |
FileNotFoundError | Intentas abrir un archivo que no existe. |
PermissionError | No tienes permisos para leer o escribir un archivo. |
KeyboardInterrupt | El usuario ha pulsado Ctrl+C. |
ImportError | No se puede importar un módulo (no instalado, mal escrito). |
EAFP: pide perdón en vez de permiso
Existen dos formas de protegerse contra errores antes de que ocurran. La primera es comprobar antes de actuar (look before you leap); la segunda es intentar y capturar (easier to ask forgiveness than permission, abreviado EAFP). En Python, la segunda es la idiomática:
# estilo LBYL: comprobar antes
import os
if os.path.exists("datos.txt"):
with open("datos.txt") as f:
contenido = f.read()
else:
contenido = ""
# estilo EAFP: intentar y manejar el fallo
try:
with open("datos.txt") as f:
contenido = f.read()
except FileNotFoundError:
contenido = ""
Las dos formas producen el mismo resultado. La EAFP es preferida en Python por dos razones: evita una race condition (entre el exists() y el open() el archivo podría desaparecer) y suele ser más rápida cuando el caso de error es raro. La diferencia es pequeña en la práctica, pero conviene saber que la comunidad Python tiende al estilo "intenta y captura".
En la próxima lección: Python en la vida real: un panorama con fragmentos completos que muestran qué aspecto tiene el código cuando se usa para resolver problemas concretos.