Errores y excepciones
try, except, else y finally: capturar lo que puede fallar, lanzar errores con intención y leer el traceback sin miedo
Un programa real falla: archivos ausentes, datos sucios y entradas inválidas. Aquí aprendes a capturar solo lo que esperas con try/except/else/finally, a lanzar excepciones propias con mensajes útiles, a encadenar causas y a leer un traceback de abajo hacia arriba.
- Usar try, except, else y finally sabiendo qué se ejecuta en cada caso
- Elegir el tipo de excepción adecuado y capturar solo lo esperado
- Definir excepciones personalizadas que lleven información del error
- Propagar o encadenar un error con raise y raise ... from
- Leer un traceback e identificar la línea y la causa real
- Evitar los bloques que silencian errores y explican por qué son peligrosos
Antes necesitas: Composición y diseño de clases
En esta lección
- ¿Qué es una excepción?
- ¿Para qué sirven?
- ¿Cómo funciona por dentro?
- Sintaxis
- Ejemplo básico: dividir sin romper el programa
- Ejemplo intermedio: excepciones propias con datos
- Caso práctico: procesar un lote y anotar las filas malas
- Visualización: el recorrido de try a finally
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Un programa que solo funciona cuando todo sale bien funciona casi nunca. El archivo que esperabas no está, la red se corta, el usuario escribe «doce» donde pedías un número y el divisor que parecía seguro resulta ser cero. Nada de eso es un error de sintaxis: el código está bien escrito y, aun así, algo falla en tiempo de ejecución.
Una excepción es la forma que tiene Python de decir «no puedo completar esta operación». Aprender a tratarlas no es añadir try por todas partes: es decidir qué fallos sabes resolver, cuáles debes reportar y cuáles deben detener el programa antes de corromper datos.
¿Qué es una excepción?
Una excepción es un objeto que representa una condición anómala. Cuando una función no puede cumplir su contrato, lanza (raise) ese objeto. Si nadie lo captura, el intérprete detiene el programa, imprime el traceback y termina con un código de salida distinto de cero.
Piensa en una alarma de incendio: no apaga el fuego, avisa de que algo va mal y deja que actúe quien esté preparado. Si nadie responde, el edificio se desaloja. En Python, el try/except es el sistema que responde; el traceback es el registro de dónde empezó el fuego.
No todas las excepciones son errores de programación. FileNotFoundError al leer un archivo que aún no existe es una condición esperable; TypeError por sumar un texto y un número es un fallo de tu lógica. La primera se maneja, la segunda se corrige.
¿Para qué sirven?
- Validar entradas: rechazar una edad negativa o un precio vacío antes de que el dato entre al sistema.
- Sobrevivir a fallos externos: un archivo ausente, una API caída o un JSON corrupto no deben tumbar todo el proceso.
- Informar con precisión: un mensaje que dice qué fila falló y por qué ahorra horas de depuración.
- Mantener el estado consistente: si un cobro falla, no debe quedar registrado a medias.
- Procesar por lotes: descartar las filas malas, anotarlas y seguir con el resto del informe.
- Separar responsabilidades: la función que valida lanza el error; quien llama decide qué hacer con él.
¿Cómo funciona por dentro?
Cuando se lanza una excepción, Python interrumpe la ejecución normal y empieza a propagarla: si la instrucción actual está dentro de un try, busca un except compatible; si no lo encuentra, la función termina y el objeto sube a quien la llamó. El proceso se repite hasta que alguien la captura o hasta llegar al intérprete.
El traceback es la lista de marcos que la excepción atravesó. Se lee de abajo hacia arriba: la última línea tiene el tipo y el mensaje, y justo encima está la línea exacta donde se lanzó. Las anteriores son el camino de llamadas. Cuando veas cien líneas de biblioteca, busca la primera que sea tu archivo.
Todas las excepciones heredan de BaseException. De ahí cuelga Exception, que agrupa las que un programa normal debería manejar: ValueError, TypeError, KeyError, ZeroDivisionError, FileNotFoundError. Captura Exception, no BaseException: esta última incluye KeyboardInterrupt y SystemExit, que existen para que puedas detener el programa.
Un try que no falla cuesta casi nada. Lo caro es lanzar y capturar excepciones, así que no las uses como un if disfrazado en un bucle de millones de iteraciones.
Sintaxis
texto = "42"
try:
valor = int(texto)
except ValueError as error:
print("No es un número:", error)
except (TypeError, KeyError):
print("Entrada con el tipo o la clave equivocados")
else:
print("Conversión correcta:", valor)
finally:
print("Este bloque se ejecuta siempre")
class ErrorDeDato(Exception):
"""Una fila del archivo no se puede procesar."""
if not texto:
raise ErrorDeDato("el texto está vacío")
try:
try:
numero = int("doce")
except ValueError as error:
raise ErrorDeDato("no se pudo convertir") from error
except ErrorDeDato as error:
print("ErrorDeDato:", error)| Cláusula | Cuándo se ejecuta |
|---|---|
try | Siempre, rodeando el código que puede fallar |
except Tipo | Si se lanza una excepción de ese tipo o de una subclase |
else | Solo si el try terminó sin excepciones |
finally | Siempre: con error, sin error, con return o con break |
raise Error(...) | Lanza una excepción nueva |
raise ... from error | Lanza una nueva y conserva la original como causa |
class ErrorDeDato(Exception):
pass
def convertir(texto):
try:
return int(texto)
except ValueError as error:
raise ErrorDeDato("no es un número: " + texto) from error
for texto in ["42", "cuarenta"]:
try:
print(convertir(texto))
except ErrorDeDato as error:
print("Error:", error)#include <iostream>
#include <stdexcept>
#include <string>
int convertir(const std::string& texto) {
try {
return std::stoi(texto);
} catch (const std::invalid_argument&) {
throw std::runtime_error("no es un número: " + texto);
}
}
int main() {
try {
std::cout << convertir("42") << '\n';
std::cout << convertir("cuarenta") << '\n';
} catch (const std::runtime_error& error) {
std::cerr << "Error: " << error.what() << '\n';
return 1;
}
return 0;
}Python captura por tipo y permite encadenar la causa con `from`. C++ captura con `catch` por referencia y no tiene `finally`: la limpieza se confía a los destructores (RAII). Ambos imprimen 42 y después el mensaje de error.
| Excepción | Aparece cuando... |
|---|---|
ValueError | El tipo es correcto pero el valor no sirve: int("doce") |
TypeError | La operación no admite esos tipos: "2" + 2 |
KeyError | Falta una clave del diccionario: datos["edad"] |
IndexError | El índice está fuera de la lista: [1, 2][5] |
ZeroDivisionError | El divisor vale cero |
FileNotFoundError | La ruta no existe o no tienes permisos para leerla |
Ejemplo básico: dividir sin romper el programa
El caso clásico. La función devuelve un número o None, y el llamador puede decidir qué hacer sin que el proceso termine.
def dividir(a, b):
try:
return a / b
except ZeroDivisionError:
print("No se puede dividir entre cero")
return None
print(dividir(10, 2))
print(dividir(10, 0))La salida es 5.0, luego el mensaje y None. Solo se captura ZeroDivisionError: si a fuera un texto, el TypeError subiría y revelaría un error de programación que no debes ocultar. Un except: a secas capturaría también eso y convertiría un bug evidente en un None silencioso.
Ejemplo intermedio: excepciones propias con datos
Cuando varias funciones comparten una regla de negocio, conviene darle un tipo propio. Así quien llama puede distinguir «el dato no cumple la regla» de «el archivo no existe» sin mirar el texto del mensaje.
class ErrorDeValidacion(Exception):
"""La entrada no cumple las reglas del dominio."""
class EdadInvalida(ErrorDeValidacion):
def __init__(self, valor):
super().__init__(f"edad inválida: {valor}")
self.valor = valor
def validar_edad(edad):
if not isinstance(edad, int):
raise TypeError("la edad debe ser un entero")
if edad < 0 or edad > 120:
raise EdadInvalida(edad)
return edad
for valor in [-5, "20", 30]:
try:
print("ok", validar_edad(valor))
except EdadInvalida as error:
print("EdadInvalida:", error, "->", error.valor)
except TypeError as error:
print("TypeError:", error)La salida es EdadInvalida: edad inválida: -5 -> -5, TypeError: la edad debe ser un entero y ok 30. EdadInvalida hereda de ErrorDeValidacion, así que un except ErrorDeValidacion también la capturaría: esa es la utilidad de la jerarquía. El atributo valor guarda el dato rechazado, algo que un mensaje de texto no siempre permite recuperar.
Caso práctico: procesar un lote y anotar las filas malas
Un informe de ventas recibe filas de un CSV. Una fila con la columna unidades no numérica no debe detener las otras cien. El objetivo es separar lo que se pudo calcular de lo que falló, con un motivo legible por cada error.
class ErrorDeDato(Exception):
"""Una fila del archivo no se puede procesar."""
def convertir(fila):
try:
producto = fila["producto"].strip()
unidades = int(fila["unidades"])
precio = float(fila["precio"])
except KeyError as error:
raise ErrorDeDato(f"falta la columna {error}") from error
except ValueError as error:
raise ErrorDeDato(f"valor no numérico en {fila}") from error
if unidades < 0 or precio <= 0:
raise ErrorDeDato(f"valores fuera de rango en {producto}")
return producto, unidades * precio
filas = [
{"producto": "teclado", "unidades": "3", "precio": "120.0"},
{"producto": "monitor", "unidades": "x", "precio": "900.0"},
{"producto": "mouse", "unidades": "5"},
{"producto": "cable", "unidades": "-2", "precio": "15.0"},
]
total = 0
errores = []
for fila in filas:
try:
producto, importe = convertir(fila)
except ErrorDeDato as error:
errores.append(str(error))
else:
total += importe
print("ok:", producto, importe)
print("total:", total)
print("errores:", len(errores))
for error in errores:
print("-", error)La salida empieza con ok: teclado 360.0, sigue con total: 360.0, errores: 3 y tres líneas de motivo: el valor no numérico, la columna que falta y los valores negativos. El else del try solo se ejecuta cuando convertir no lanzó nada, así que el total nunca suma una fila a medias. Y raise ... from error conserva la causa original: si depuras, el traceback muestra tanto ValueError como ErrorDeDato.
convertir no imprime: lanza un error con la información y deja que el bucle decida si lo acumula o lo relanza. Así la misma función sirve en un script, en una API o en una prueba.
Visualización: el recorrido de try a finally
Sigue una división entre cero. Observa el orden: se lanza en el try, se captura en el except, el else se salta y el finally se ejecuta antes de que el return llegue a quien llamó.
def dividir(a, b): try: resultado = a / b except ZeroDivisionError as error: print("Error:", error) return None else: print("Sin errores") return resultado finally: print("Termina dividir")print(dividir(10, 0))dividir(10, 0). La función entra con a = 10 y b = 0; el try aún no ha ejecutado nada.Sin variables en este paso.
—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Errores comunes
| Mensaje | Causa habitual | Cómo se arregla |
|---|---|---|
NameError: name 'error' is not defined | Usaste la variable del except fuera de su bloque | Guarda str(error) dentro del bloque o asigna una variable propia |
ValueError: invalid literal for int() with base 10: 'doce' | Convertiste texto no numérico sin capturar el fallo | Captura ValueError o valida el texto antes de convertirlo |
TypeError: catching classes that do not inherit from BaseException is not allowed | Escribiste except "error": en lugar de una clase | Usa except ValueError: o el tipo de excepción que corresponda |
IndentationError: expected an indented block | El try o el except quedaron sin cuerpo | Indenta al menos una instrucción en cada bloque |
ZeroDivisionError: division by zero | Dividiste entre un valor que resultó ser cero | Comprueba el divisor o captura la excepción en el nivel que sepa resolverla |
texto = "doce"
try:
edad = int(texto)
except:
pass
print(edad)texto = "doce"
try:
edad = int(texto)
except ValueError:
edad = 0
print(edad)En el código incorrecto, el `except` vacío oculta el `ValueError` y `edad` nunca se asigna: el programa falla después con `NameError: name 'edad' is not defined`, un mensaje que no menciona el problema real. En el correcto, la captura es específica y el valor por defecto es una decisión visible.
El NameError del ejemplo ilustra por qué conviene guardar el mensaje: Python elimina la variable error al salir del except. Si lo necesitas después, cópialo a otra variable dentro del bloque.
Buenas prácticas
- Captura el tipo más específico que sepas manejar; deja el resto subir.
- Nunca uses
except:desnudo niexcept Exception: pass. - Usa
elsepara el código que solo debe ejecutarse si no hubo error, y mantenlo fuera deltry. - Usa
finally(o mejorwith) para liberar recursos. - Lanza excepciones con un mensaje que explique qué se esperaba y qué llegó.
- Define excepciones propias cuando el error tenga significado de dominio.
- Conserva la causa con
raise Nueva(...) from error; usafrom Nonesolo si la original confunde. - No uses excepciones para el flujo normal: si un caso es esperable y frecuente, resuélvelo con una condición.
Ejercicios
Conversor con excepción de formato
ErrorDeFormato(campo, valor) que herede de Exception, guarde ambos datos y construya un mensaje como edad: 'treinta' no es un entero. Escribe a_entero(texto, campo) que convierta con int y, si falla con ValueError o TypeError, lance ErrorDeFormato conservando la causa. Recorre [("edad", "30"), ("edad", "treinta"), ("peso", "72.5")] e imprime el resultado o el error con su campo.Pista
Captura (TypeError, ValueError) en una sola cláusula y usa raise ... from error. En el bucle, except ErrorDeFormato as error: te da acceso a error.campo.
Configuración validada con else
leer_puerto(config) que exija la clave puerto, la convierta a entero y compruebe que esté entre 1 y 65535. Cada problema debe lanzar una ErrorDeConfiguracion distinta: clave ausente, valor no numérico o puerto fuera de rango. Recorre cuatro configuraciones —"8080", sin clave, "ochenta" y "70000"— y usa else para imprimir solo los puertos válidos.Pista
Comprueba "puerto" not in config antes de convertir. Envolvé solo la conversión en try/except ValueError y lanza la nueva excepción con from error. El bucle usa try/except/else.
texto = "42" try: valor = int(texto) ValueError: print("no es un número") : print("conversión correcta:", valor)
La cláusula que captura empieza por `except`; la que se ejecuta solo sin error es `else`.
Soluciones explicadas
En el conversor, la tentación es devolver None cuando algo falla. El problema aparece en el llamador: None no distingue «el campo estaba vacío» de «el usuario escribió letras», y tarde o temprano acaba en una operación aritmética con un mensaje incomprensible. Una excepción con campo y valor obliga a tomar una decisión explícita.
En la configuración, el else no es decorativo: separa el camino feliz del camino de error. Si imprimes dentro del try, una excepción posterior en el print también se atribuiría al bloque de conversión. Mantener una sola operación por try reduce el riesgo de capturar un error que no era el que esperabas.
Sobre el traceback: cuando un error sube desde una biblioteca, lee la última línea y busca hacia arriba la primera entrada de tu archivo. raise ... from error conserva la causa; from None la oculta cuando solo añade ruido. Ninguna sustituye a un mensaje que explique qué intentabas hacer.
Resumen
- Una excepción es un objeto que interrumpe el flujo normal y se propaga hasta que alguien la captura.
tryrodea el código arriesgado;except Tipocaptura;elsecorre sin error;finallycorre siempre.- Captura el tipo más específico posible y no silencies errores con
except: pass. raiselanza una excepción;raise ... from errorconserva la causa original.- Una excepción personalizada hereda de
Exceptiony puede llevar atributos con el dato rechazado. - El traceback se lee de abajo hacia arriba: tipo, mensaje y línea del fallo.
- EAFP prefiere intentar y capturar cuando el fallo es raro;
dict.getes mejor si el caso es habitual. - Si no sabes qué hacer con el error, no lo captures: déjalo subir con su contexto.
Evaluación
En un bloque try/except/else/finally, ¿cuándo se ejecuta else?
¿Qué diferencia hay entre except ValueError: y except:?
Una función lanza EdadInvalida, que hereda de ErrorDeValidacion, que hereda de Exception. ¿Qué cláusula la captura en try/except ErrorDeValidacion?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.