📘 Contenido del Tema 3.2 — Manejo de Excepciones en Python
💡 Nota: Este módulo te enseñará a escribir programas que no se detienen ante los errores, sino que los manejan inteligentemente.
Aprenderás cómo capturar, identificar, prevenir y lanzar tus propias excepciones, además de técnicas para depurar y verificar el correcto funcionamiento de tu código.
Parte 1 — Introducción al Manejo de Excepciones en Python
En cualquier lenguaje de programación, los errores son inevitables.
Sin embargo, un buen programador no busca evitarlos por completo, sino anticiparlos y manejarlos correctamente.
En Python, esta tarea se realiza mediante el sistema de excepciones.
1.1 ¿Qué es una excepción?
Una excepción es un evento que ocurre durante la ejecución de un programa y que interrumpe el flujo normal de las instrucciones.
Cuando Python encuentra una situación inesperada (como dividir entre cero o intentar abrir un archivo inexistente), genera una excepción y detiene el programa.
# Ejemplo: división entre cero
print("Inicio del programa")
resultado = 10 / 0
print("Fin del programa")
Salida:
Inicio del programa
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
ZeroDivisionError: division by zero
El programa se detiene en el momento del error y muestra una traza del error (traceback),
indicando el tipo de excepción y la línea donde ocurrió.
Exception
.1.2 Por qué es importante manejarlas
Manejar las excepciones permite que tu programa sea más estable, seguro y profesional.
Un sistema robusto no se detiene ante un error; en cambio, lo detecta, lo comunica y continúa funcionando.
Imagina que tu aplicación web deja de funcionar porque un usuario introdujo un valor inválido.
Con manejo de excepciones, puedes mostrar un mensaje amistoso en lugar de un fallo técnico.
try:
numero = int(input("Introduce un número entero: "))
print(f"Has introducido: {numero}")
except ValueError:
print("Error: Debes introducir un número válido.")
Salida:
Introduce un número entero: hola
Error: Debes introducir un número válido.
El programa no se detiene; simplemente reacciona ante la excepción y sigue funcionando.
1.3 Tipos de errores en Python
En Python distinguimos dos grandes categorías de errores:
Tipo de error | Descripción | Ejemplo |
---|---|---|
Error de sintaxis | Ocurre cuando el código no sigue las reglas del lenguaje. | print("Hola" → falta paréntesis) |
Excepción en tiempo de ejecución | Ocurre cuando el programa se ejecuta correctamente, pero surge una situación inesperada. | 10 / 0 → ZeroDivisionError |
Las excepciones son, por tanto, errores que ocurren durante la ejecución y pueden ser manejadas.
1.4 Ejemplo de ejecución sin manejo de errores
Veamos un ejemplo sin manejo de errores:
def dividir(a, b):
return a / b
print(dividir(10, 2))
print(dividir(5, 0))
print("Este mensaje nunca se mostrará")
Salida:
5.0
Traceback (most recent call last):
File "<stdin>", line 4, in <module>
ZeroDivisionError: division by zero
El programa se detiene en la línea donde ocurre el error.
1.5 Ejemplo con manejo de excepciones
def dividir(a, b):
try:
resultado = a / b
return resultado
except ZeroDivisionError:
print("Error: No se puede dividir entre cero.")
return None
print(dividir(10, 2))
print(dividir(5, 0))
print("El programa sigue ejecutándose correctamente.")
Salida:
5.0
Error: No se puede dividir entre cero.
El programa sigue ejecutándose correctamente.
Ahora el programa controla el fallo, muestra un mensaje y continúa su ejecución sin detenerse.
1.6 Ventajas del manejo de excepciones
- ✅ Permite controlar el flujo del programa ante errores imprevistos.
- ✅ Mejora la experiencia del usuario final.
- ✅ Facilita la depuración y el mantenimiento del código.
- ✅ Evita pérdidas de datos o comportamientos inesperados.
- ✅ Aumenta la robustez de aplicaciones grandes o críticas.
1.7 Buenas prácticas iniciales
- ✅ Usa siempre bloques
try
yexcept
para operaciones propensas a error (divisiones, conversiones, archivos). - ✅ Específica el tipo exacto de excepción (por ejemplo,
ValueError
oZeroDivisionError
). - ✅ No uses
except:
sin tipo — puede ocultar errores graves. - ✅ Agrega mensajes descriptivos para facilitar el diagnóstico.
- ✅ Evita abusar del manejo de excepciones para controlar la lógica normal del programa.
1.8 Mini-quiz
- ¿Qué es una excepción?
- ¿Cuál es la diferencia entre un error de sintaxis y una excepción?
- ¿Por qué es importante manejar los errores?
💡 Ver respuestas
- 1️⃣ Es un evento inesperado que interrumpe el flujo normal del programa.
- 2️⃣ El error de sintaxis ocurre antes de ejecutar; la excepción ocurre durante la ejecución.
- 3️⃣ Porque permite mantener el programa funcionando sin interrupciones.
1.9 Prácticas guiadas
Ejercicio 1 — Conversión segura
try:
numero = int(input("Introduce un número entero: "))
print(f"Número válido: {numero}")
except ValueError:
print("Debes ingresar un número entero válido.")
Ejercicio 2 — División segura
def dividir_seguro(a, b):
try:
return a / b
except ZeroDivisionError:
return "Error: división entre cero."
print(dividir_seguro(10, 0))
Ejercicio 3 — Multiples operaciones
try:
x = int(input("Número 1: "))
y = int(input("Número 2: "))
print("Resultado:", x / y)
except ValueError:
print("Introduce solo números.")
except ZeroDivisionError:
print("No se puede dividir entre cero.")
finally:
print("Fin de la operación.")
Parte 2 — Errores comunes en Python
Antes de aprender a capturar y manejar excepciones, es esencial conocer los errores más frecuentes que pueden aparecer al programar en Python.
Comprenderlos te ayudará a anticiparte a ellos y a escribir código más sólido y confiable.
2.1 Tipos generales de errores
Podemos clasificar los errores en dos categorías principales:
Tipo | Cuándo ocurre | Ejemplo |
---|---|---|
Errores de sintaxis | Cuando el código viola las reglas del lenguaje. | print("Hola" → Falta el paréntesis de cierre) |
Excepciones en tiempo de ejecución | Cuando el código es sintácticamente correcto pero falla al ejecutarse. | 10 / 0 → ZeroDivisionError |
2.2 Errores de sintaxis
Los errores de sintaxis (SyntaxError) se detectan antes de ejecutar el programa.
Indican que hay un problema con la estructura del código: faltan paréntesis, comillas o dos puntos, por ejemplo.
# Error de sintaxis: falta el paréntesis de cierre
print("Hola mundo"
Salida:
File "<stdin>", line 1
print("Hola mundo"
^
SyntaxError: unexpected EOF while parsing
Este tipo de error se corrige antes de ejecutar el código.
Los editores modernos (como VS Code o PyCharm) suelen marcarlo automáticamente.
2.3 Excepciones en tiempo de ejecución
Las excepciones ocurren mientras el programa está en marcha.
Aunque el código sea correcto, una operación puede fallar por causas imprevistas.
# División entre cero
resultado = 10 / 0
print(resultado)
Salida:
ZeroDivisionError: division by zero
En este caso, Python detecta un intento de dividir entre cero y lanza una excepción.
2.4 Excepciones más comunes en Python
A continuación, una tabla con los errores más frecuentes que encontrarás al programar:
Excepción | Descripción | Ejemplo |
---|---|---|
ValueError | El tipo de dato es correcto, pero el valor no tiene sentido. | int("hola") |
TypeError | Se intenta operar entre tipos incompatibles. | "3" + 2 |
ZeroDivisionError | Se intenta dividir entre cero. | 10 / 0 |
IndexError | Índice fuera de rango en una lista o tupla. | lista = [1, 2]; print(lista[5]) |
KeyError | Se accede a una clave inexistente en un diccionario. | dic = {"a": 1}; print(dic["b"]) |
FileNotFoundError | Se intenta abrir un archivo que no existe. | open("archivo.txt") |
AttributeError | Se llama a un atributo o método que no existe. | numero = 10; numero.append(5) |
NameError | Se usa una variable que no ha sido definida. | print(variable_inexistente) |
ImportError | El módulo que intentas importar no se encuentra. | import modulo_que_no_existe |
MemoryError | El sistema se queda sin memoria. | [0] * 999999999999 |
2.5 Ejemplos prácticos
Ejemplo 1 — ValueError
numero = int("abc")
Salida:
ValueError: invalid literal for int() with base 10: 'abc'
Ejemplo 2 — TypeError
print("Edad: " + 25)
Salida:
TypeError: can only concatenate str (not "int") to str
Ejemplo 3 — IndexError
lista = [1, 2, 3]
print(lista[5])
Salida:
IndexError: list index out of range
Ejemplo 4 — FileNotFoundError
f = open("archivo_inexistente.txt")
Salida:
FileNotFoundError: [Errno 2] No such file or directory: 'archivo_inexistente.txt'
2.6 Cómo identificar el tipo de excepción
Cuando ocurre un error, Python muestra el nombre del tipo de excepción en el mensaje de error.
Esto te ayuda a saber qué tipo de problema ocurrió para manejarlo correctamente con except
.
try:
x = int("Hola")
except Exception as e:
print("Se produjo un error de tipo:", type(e).__name__)
Salida:
Se produjo un error de tipo: ValueError
2.7 Cómo prevenir errores comunes
- ✅ Verifica los datos de entrada antes de convertir tipos (
int()
,float()
…). - ✅ Comprueba que un archivo existe antes de abrirlo.
- ✅ Asegúrate de que los índices están dentro del rango de la lista.
- ✅ Evita dividir entre cero comprobando el divisor previamente.
- ✅ Define todas las variables antes de usarlas.
- ✅ Importa correctamente los módulos y dependencias.
2.8 Mini-quiz
- ¿Qué diferencia hay entre un SyntaxError y un ValueError?
- ¿Qué tipo de error ocurre si intentas acceder a un índice inexistente de una lista?
- ¿Qué error se lanza al dividir entre cero?
💡 Ver respuestas
- 1️⃣ El primero ocurre antes de ejecutar el programa; el segundo durante su ejecución.
- 2️⃣
IndexError
. - 3️⃣
ZeroDivisionError
.
2.9 Prácticas guiadas
Ejercicio 1 — Detección de errores
Identifica y corrige los errores en el siguiente código:
lista = [1, 2, 3]
print(lista[3])
print(10 / 0)
valor = int("Hola")
Ejercicio 2 — Mostrar tipo de excepción
try:
resultado = 10 / 0
except Exception as e:
print("Error detectado:", type(e).__name__)
Ejercicio 3 — Comprobación previa
numerador = 10
denominador = 0
if denominador != 0:
print("Resultado:", numerador / denominador)
else:
print("Error: no se puede dividir entre cero.")
Parte 3 — Bloques try
, except
, else
, finally
El manejo de excepciones en Python se basa en cuatro piezas clave:
try
(intenta ejecutar), except
(captura errores),
else
(se ejecuta si no hubo errores) y finally
(se ejecuta siempre, ocurra o no una excepción). Usadas con cabeza, hacen tu código robusto, legible y mantenible.
3.1 Visión general
Bloque | Cuándo se ejecuta | Uso típico |
---|---|---|
try | Siempre; aquí pones el código que puede fallar | Operaciones frágiles (I/O, parseo, divisiones, red) |
except | Solo si ocurre una excepción en el try | Manejar casos de error específicos (p. ej., ValueError ) |
else | Solo si no ocurrió ninguna excepción | Trabajos que dependen del éxito del try |
finally | Siempre, haya o no excepción | Liberar recursos (cerrar archivos, conexiones, locks) |
3.2 Sintaxis básica
try:
# Código que podría lanzar una excepción
resultado = 10 / x
except ZeroDivisionError:
# Se ejecuta si pasó ZeroDivisionError
print("No se puede dividir entre cero.")
Con else
y finally
:
try:
resultado = 10 / x
except ZeroDivisionError:
print("No se puede dividir entre cero.")
else:
# Solo si no hubo excepción
print("Resultado:", resultado)
finally:
# Siempre
print("Fin del proceso (liberar recursos, etc.)")
else
para lo que depende del éxito del try
y
finally
para limpieza. Mantén el try
lo más pequeño posible (solo lo que puede fallar).3.3 Múltiples except
y captura de varias excepciones
Captura tipos específicos primero; de lo contrario, el bloque general “tapa” a los concretos.
try:
n = int(input("Número: "))
print(10 / n)
except ValueError:
print("Debes escribir un número entero.")
except ZeroDivisionError:
print("No se puede dividir entre cero.")
except Exception as e:
print("Error inesperado:", type(e).__name__)
O agrupa varias excepciones relacionadas en una tupla:
try:
# lectura y conversión
dato = float(input("Precio: "))
except (ValueError, TypeError) as e:
print("Entrada inválida:", e)
3.4 Captura de la excepción como objeto
Accede al mensaje/atributos del error con as e
para loguear o mostrar detalles.
try:
with open("config.yaml") as f:
contenido = f.read()
except FileNotFoundError as e:
print("Archivo no encontrado:", e.filename)
3.5 Uso correcto de else
else
se ejecuta solo si todo fue bien en el try
. Úsalo para evitar capturar errores de código que no debe estar en el try
.
try:
n = int(input("Edad: "))
except ValueError:
print("Edad inválida.")
else:
# Solo si la conversión fue OK
categoria = "Mayor" if n >= 18 else "Menor"
print("Categoría:", categoria)
3.6 Uso correcto de finally
(limpieza)
Se usa para liberar recursos siempre: cerrar archivos, conexiones, deshacer locks…
f = None
try:
f = open("datos.txt")
datos = f.read()
# procesar datos...
except FileNotFoundError:
print("No existe el archivo.")
finally:
if f:
f.close() # se ejecuta ocurra o no la excepción
Con context manager (with
) no necesitas finally
para cerrar:
try:
with open("datos.txt") as f:
datos = f.read()
except FileNotFoundError:
print("No existe el archivo.")
with
siempre que exista un context manager. Simplifica y evita fugas de recursos.3.7 Anidación y re-lanzamiento (raise
)
Puedes capturar, registrar y volver a lanzar (para que otro nivel lo maneje):
def cargar_config(ruta):
try:
with open(ruta) as f:
return f.read()
except FileNotFoundError as e:
print("[LOG] faltante:", e.filename)
raise # re-lanza la misma excepción
try:
cfg = cargar_config("app.cfg")
except FileNotFoundError:
print("Config no encontrada. Usando valores por defecto.")
3.8 Patrones recomendados (y anti-patrones)
Haz ✅ | No hagas ❌ | Por qué |
---|---|---|
Captura excepciones específicas | except Exception: para todo | Lo genérico oculta bugs difíciles |
Minimiza el bloque try | Envolver medio archivo en try | Acota el punto de fallo y mejora legibilidad |
Usa else para post-éxito | Poner lógica “feliz” dentro del try | Evitas capturar errores que no deben |
Usa finally o with | Olvidar liberar recursos | Previene fugas, locks colgados, archivos abiertos |
Ordena de específico → general | Genérico antes que específico | El general “come” a los específicos |
3.9 Flujo completo con try/except/else/finally
def dividir(a: float, b: float) -> float | None:
f = None
try:
# Solo lo frágil
resultado = a / b
except ZeroDivisionError:
print("No se puede dividir entre cero.")
return None
else:
# Éxito: ya puedes usar 'resultado'
return resultado
finally:
# Siempre
# (Si tuvieras recursos abiertos, ciérralos aquí)
pass
print(dividir(10, 2)) # 5.0
print(dividir(10, 0)) # None
3.10 Ejemplos prácticos
3.10.1 Lectura segura de archivo + parseo
def leer_entero(ruta):
try:
with open(ruta) as f:
valor = int(f.read().strip())
except FileNotFoundError:
print("Archivo no encontrado.")
return None
except ValueError:
print("El contenido no es un entero válido.")
return None
else:
return valor
print(leer_entero("input.txt"))
3.10.2 Red: reintentos básicos
import time, random
def fetch():
# Simulación: falla 2 de cada 3 veces
if random.choice([True, False, False]):
raise ConnectionError("Fallo de red simulado")
return "OK"
def fetch_con_reintentos(max_reintentos=3):
for intento in range(1, max_reintentos + 1):
try:
return fetch()
except ConnectionError as e:
print(f"Intento {intento}: {e}")
time.sleep(0.5)
return None
print(fetch_con_reintentos())
3.11 Mini-quiz
- ¿Cuándo se ejecuta el bloque
else
de untry
? - ¿Por qué conviene ordenar los
except
del más específico al más general? - Completa para capturar dos tipos a la vez:
try: x = int("hola"); print(10/x) except (__________, __________) as e: print("Error:", type(e).__name__)
Respuesta
ValueError
,ZeroDivisionError
3.12 Prácticas guiadas
Práctica 1 — Conversión robusta
Pide un número y devuélvelo como float
. Si hay error, devuelve None
. Usa else
para imprimir “OK”.
def a_float(s: str) -> float | None:
try:
v = float(s)
except ValueError:
return None
else:
print("OK")
return v
Práctica 2 — Copia de archivo con limpieza
Lee de origen.txt
y escribe en destino.txt
. Maneja FileNotFoundError
y usa with
.
try:
with open("origen.txt", "r") as src, open("destino.txt", "w") as dst:
dst.write(src.read())
except FileNotFoundError as e:
print("Falta:", e.filename)
else:
print("Copia realizada")
Práctica 3 — Registro y re-lanzamiento
Envuelve una función que pueda fallar; registra el error y vuelve a lanzarlo para que lo maneje el llamador.
def envoltorio(fn, *args, **kwargs):
try:
return fn(*args, **kwargs)
except Exception as e:
print("[LOG]:", type(e).__name__, e)
raise
Parte 4 — Lanzamiento de excepciones con raise
Manejar excepciones es la mitad del juego; la otra mitad es lanzarlas cuando detectamos
condiciones inválidas. Con raise
comunicamos de forma explícita que algo no cumple
las precondiciones o que el flujo debe interrumpirse y ser atendido por el llamador.
4.1 Sintaxis básica de raise
# Lanzar una excepción con mensaje
raise ValueError("El parámetro 'edad' debe ser >= 0")
Puedes lanzar cualquier instancia de excepción que herede de Exception
(o una subclase).
if edad < 0:
raise ValueError("Edad no puede ser negativa")
4.2 ¿Cuándo usar raise
?
- 🔹 Validación de entradas (precondiciones).
- 🔹 Estados imposibles o inconsistentes (invariantes rotos).
- 🔹 Operaciones que no pueden continuar con datos corruptos o ausentes.
- 🔹 En librerías: señalar error al código cliente, en lugar de “tragar” el problema.
def transferir(origen, destino, importe):
if importe <= 0:
raise ValueError("El importe debe ser positivo")
if origen.saldo < importe:
raise RuntimeError("Saldo insuficiente")
origen.saldo -= importe
destino.saldo += importe
4.3 Re-lanzamiento de excepciones (propagación)
Puedes capturar para registrar/limpiar y luego re-lanzar la misma excepción con raise
“en vacío”.
def leer_config(ruta):
try:
with open(ruta) as f:
return f.read()
except FileNotFoundError as e:
print("[LOG] Config faltante:", e.filename)
raise # Propaga al llamador
4.4 Encadenamiento de excepciones con from
Útil cuando quieres traducir una excepción de bajo nivel a otra del dominio, conservando el contexto original.
class ConfigError(Exception):
pass
def parsear_config(texto):
try:
return int(texto) # ejemplo artificial
except ValueError as err:
raise ConfigError("Formato de configuración inválido") from err
El traceback mostrará la excepción de alto nivel y la causa original (desarrollo más fácil).
4.5 Excepciones personalizadas
Crea tus propias excepciones para expresar semántica de dominio. Hereda de Exception
o de una subclase adecuada.
class AppError(Exception):
"""Error base de la aplicación."""
class ValidacionError(AppError):
"""Datos de entrada inválidos."""
class PermisoDenegado(AppError):
"""Operación no autorizada."""
def crear_usuario(nombre: str, edad: int):
if not nombre:
raise ValidacionError("El nombre es obligatorio")
if edad < 0:
raise ValidacionError("La edad no puede ser negativa")
# ...
return {"nombre": nombre, "edad": edad}
4.6 Mensajes claros y datos útiles
Redacta mensajes precisos: qué falló, por qué y (si procede) cómo resolverlo.
Añade contexto (valores, rutas, ids) sin filtrar datos sensibles.
def establecer_iva(porcentaje: float):
if not 0 <= porcentaje <= 50:
raise ValueError(f"IVA fuera de rango: {porcentaje}. Debe estar entre 0 y 50")
4.7 Patrones habituales de validación
Situación | Excepción sugerida | Ejemplo |
---|---|---|
Entrada con tipo correcto pero valor inválido | ValueError | raise ValueError("edad >= 0") |
Operación en estado inadecuado | RuntimeError | raise RuntimeError("no inicializado") |
Acceso no autorizado | PermissionError o personalizada | raise PermissionError("requiere rol admin") |
Recurso ausente | FileNotFoundError / personalizada | raise FileNotFoundError(ruta) |
4.8 Anti-patrones (evita esto)
- ❌ Lanzar
Exception
genérica sin semántica (difícil de manejar aguas arriba). - ❌ Usar excepciones para lógica “normal” (control de flujo que no es excepcional).
- ❌ Mensajes vagos: “error”, “falló algo” (no ayudan a nadie).
- ❌ Ocultar la causa al traducir errores (no usar
from
).
4.9 Patrón “validar y continuar”
def procesar_pedido(pedido: dict) -> None:
requerido = ("id", "cliente", "importe")
faltantes = [k for k in requerido if k not in pedido]
if faltantes:
raise KeyError(f"Pedido incompleto. Faltan: {', '.join(faltantes)}")
if pedido["importe"] <= 0:
raise ValueError("El importe debe ser positivo")
# OK: continuar con el flujo principal
print("Pedido procesado:", pedido["id"])
4.10 Integración con try/except
y re-intentos
import time
class TransientError(Exception):
pass
def llamar_servicio():
# simulación de fallo intermitente
raise TransientError("Timeout en servicio X")
def con_reintentos(fn, intentos=3, espera=0.2):
for i in range(1, intentos + 1):
try:
return fn()
except TransientError as e:
print(f"Reintento {i}/{intentos}: {e}")
time.sleep(espera)
raise RuntimeError("Servicio no disponible tras reintentos")
4.11 Excepciones personalizadas con datos extra
class ApiError(Exception):
def __init__(self, status: int, mensaje: str, *, endpoint: str | None = None):
super().__init__(f"[{status}] {mensaje}")
self.status = status
self.endpoint = endpoint
# Uso:
# raise ApiError(404, "Recurso no encontrado", endpoint="/v1/items/7")
Así puedes inspeccionar atributos en el except
y decidir la respuesta.
4.12 Buenas prácticas
- ✅ Lanza excepciones específicas y documentadas.
- ✅ Usa mensajes que indiquen el qué, el por qué y (si aplica) el cómo.
- ✅ Encadena con
from
al traducir errores de bajo nivel. - ✅ Diseña una jerarquía ligera de errores de dominio.
- ✅ No sustituyas validación por
try/except
si puedes comprobar barato antes.
4.13 Mini-quiz
- ¿Qué ventaja tiene
raise ... from err
frente a unraise
simple? - ¿Por qué es preferible lanzar una excepción de dominio (p. ej.,
ValidacionError
)? - Rellena:
try: leer_config("cfg.ini") except FileNotFoundError as e: raise ConfigError("No se pudo cargar") ____ e
Respuesta
from
4.14 Prácticas guiadas
Práctica 1 — Validar argumentos
def dividir(a: float, b: float) -> float:
if b == 0:
raise ZeroDivisionError("b no puede ser 0")
return a / b
Práctica 2 — Jerarquía mínima de dominio
class TiendaError(Exception): ...
class StockInsuficiente(TiendaError): ...
class ProductoInexistente(TiendaError): ...
def vender(stock: dict, sku: str, unidades: int):
if sku not in stock:
raise ProductoInexistente(f"SKU {sku} no existe")
if stock[sku] < unidades:
raise StockInsuficiente(f"Stock insuficiente para {sku}")
stock[sku] -= unidades
Práctica 3 — Traducir error bajo nivel a dominio
class CSVError(Exception): ...
def cargar_csv(ruta: str):
try:
with open(ruta, "r", encoding="utf-8") as f:
return f.read().splitlines()
except FileNotFoundError as e:
raise CSVError(f"No se encuentra el archivo: {ruta}") from e
Práctica 4 — Reintentos con error transitorio
def reintentar(fn, veces=3):
for i in range(veces):
try:
return fn()
except TimeoutError as e:
if i == veces - 1:
raise RuntimeError("Agotados reintentos") from e
Parte 5 — Jerarquía de Excepciones
Python organiza todas las excepciones en una jerarquía de clases.
Esto permite manejar los errores de forma general (capturando la base) o específica (capturando un tipo concreto).
Entender esta jerarquía te ayuda a escribir bloques except más precisos y elegantes.
5.1 Concepto de jerarquía
En Python, todas las excepciones heredan de la clase BaseException
.
Desde ella se derivan dos ramas principales:
- SystemExit, KeyboardInterrupt y GeneratorExit — usadas internamente por el intérprete.
- Exception — base de todas las excepciones que usamos habitualmente en nuestros programas.
Esto significa que si capturas Exception
, atrapas la mayoría de los errores de usuario, pero no los que detienen el intérprete.
5.2 Diagrama simplificado de jerarquía
BaseException
├── SystemExit
├── KeyboardInterrupt
├── GeneratorExit
└── Exception
├── ArithmeticError
│ ├── ZeroDivisionError
│ ├── OverflowError
│ └── FloatingPointError
├── AssertionError
├── AttributeError
├── EOFError
├── ImportError
│ └── ModuleNotFoundError
├── IndexError
├── KeyError
├── LookupError
│ ├── IndexError
│ └── KeyError
├── MemoryError
├── NameError
├── OSError
│ ├── FileNotFoundError
│ ├── PermissionError
│ └── TimeoutError
├── ReferenceError
├── RuntimeError
│ └── RecursionError
├── SyntaxError
│ └── IndentationError
├── TypeError
├── ValueError
│ └── UnicodeError
└── Warning
├── DeprecationWarning
├── UserWarning
└── SyntaxWarning
No necesitas memorizar todo el árbol, pero es útil conocer los más frecuentes y su relación de herencia.
5.3 Ejemplo práctico de herencia
Como todas las excepciones son clases, puedes comprobar su tipo y jerarquía con issubclass()
o isinstance()
.
print(issubclass(ZeroDivisionError, ArithmeticError)) # True
print(issubclass(ArithmeticError, Exception)) # True
print(issubclass(Exception, BaseException)) # True
try:
1 / 0
except ArithmeticError:
print("Se capturó un error aritmético")
Salida:
Se capturó un error aritmético
Aunque el error fue ZeroDivisionError
, Python lo considera parte de la familia ArithmeticError
,
por lo que fue capturado correctamente.
5.4 Captura genérica vs específica
Tipo de captura | Ventajas | Desventajas |
---|---|---|
Captura específicaexcept FileNotFoundError: | ✔️ Precisión ✔️ Control claro del flujo ✔️ Evita ocultar errores no relacionados | ❌ Requiere conocer las excepciones posibles |
Captura genéricaexcept Exception: | ✔️ Útil para registrar o mostrar mensajes globales | ❌ Oculta el tipo de error real ❌ Dificulta la depuración |
5.5 Captura multinivel (padre e hijo)
Si defines varios except
, el orden importa:
el bloque se evalúa de arriba abajo. Python ejecutará el primero que coincida.
try:
1 / 0
except ZeroDivisionError:
print("Error específico: división entre cero")
except ArithmeticError:
print("Error aritmético general")
Salida:
Error específico: división entre cero
Si inviertes el orden, el genérico capturaría antes al específico y el segundo bloque nunca se ejecutaría.
5.6 Herencia en excepciones personalizadas
Al crear tus propias excepciones, elige una superclase adecuada según la naturaleza del error.
class AppError(Exception):
"""Error base de la aplicación."""
class EntradaInvalida(AppError, ValueError):
"""Valor fuera de rango o formato incorrecto."""
class ConexionFallida(AppError, ConnectionError):
"""Error al conectar con el servidor."""
try:
raise ConexionFallida("No se pudo conectar al servidor API.")
except AppError as e:
print("Error de aplicación:", type(e).__name__)
Salida:
Error de aplicación: ConexionFallida
5.7 Consejos para diseñar jerarquías personalizadas
- ✅ Define una excepción base (por ejemplo,
AppError
oMiProyectoError
). - ✅ Agrupa subclases por tipo de error lógico (validación, red, base de datos, permisos, etc.).
- ✅ Mantén una jerarquía simple (2 o 3 niveles como máximo).
- ✅ Evita heredar directamente de
BaseException
(usaException
). - ✅ Documenta claramente cuándo lanzar cada tipo.
5.8 Casos prácticos
Ejemplo 1 — Captura general vs específica
try:
with open("no_existe.txt") as f:
contenido = f.read()
except FileNotFoundError:
print("El archivo no existe.")
except OSError:
print("Error general de sistema de archivos.")
except Exception:
print("Error desconocido.")
Salida:
El archivo no existe.
Ejemplo 2 — Árbol personalizado de dominio
class BancoError(Exception): ...
class CuentaError(BancoError): ...
class SaldoInsuficiente(CuentaError): ...
class CuentaNoExiste(CuentaError): ...
def retirar(cuentas, id, cantidad):
if id not in cuentas:
raise CuentaNoExiste(f"La cuenta {id} no existe.")
if cuentas[id] < cantidad:
raise SaldoInsuficiente("Saldo insuficiente.")
cuentas[id] -= cantidad
return cuentas[id]
cuentas = {"A1": 100}
try:
retirar(cuentas, "A1", 200)
except BancoError as e:
print("Error bancario:", type(e).__name__)
Salida:
Error bancario: SaldoInsuficiente
Capturar BancoError
te permite interceptar todos los errores de ese dominio,
sin perder detalle ni tener que listar cada tipo.
5.9 Inspección y depuración
Puedes examinar la jerarquía de excepciones directamente desde el intérprete:
help(Exception)
help(ArithmeticError)
help(OSError)
O imprimir el nombre de clase de una excepción capturada:
try:
x = 1 / 0
except Exception as e:
print("Tipo de excepción:", type(e).__name__)
Salida:
Tipo de excepción: ZeroDivisionError
5.10 Mini-quiz
- ¿Cuál es la clase base de todas las excepciones en Python?
- ¿Qué pasa si capturas
Exception
pero noBaseException
? - ¿Por qué es importante el orden de los bloques
except
?
💡 Ver respuestas
- 1️⃣
BaseException
. - 2️⃣ Capturas los errores normales, pero no los que detienen el intérprete (
SystemExit
,KeyboardInterrupt
…). - 3️⃣ Porque el primer bloque coincidente se ejecuta y los demás se ignoran.
5.11 Prácticas guiadas
Ejercicio 1 — Captura jerárquica
try:
resultado = 10 / 0
except ArithmeticError:
print("Error aritmético capturado correctamente")
Ejercicio 2 — Excepciones de dominio
class AppError(Exception): ...
class ValidacionError(AppError): ...
class ConexionError(AppError): ...
def ejecutar_operacion(x):
if x < 0:
raise ValidacionError("Número negativo no permitido")
if x == 0:
raise ConexionError("Conexión perdida")
return x * 10
for valor in [-1, 0, 5]:
try:
print(ejecutar_operacion(valor))
except AppError as e:
print("Error de aplicación:", type(e).__name__)
Salida:
Error de aplicación: ValidacionError
Error de aplicación: ConexionError
50
Ejercicio 3 — Verificar jerarquía personalizada
print(issubclass(ValidacionError, AppError)) # True
print(issubclass(ConexionError, Exception)) # True
print(issubclass(AppError, BaseException)) # True
Parte 6 — Depuración con assert
La sentencia assert
es una herramienta de depuración para verificar
invariantes y suposiciones internas durante el desarrollo. Si la condición es falsa,
Python lanza AssertionError
(con un mensaje opcional).
6.1 Sintaxis y comportamiento
assert condicion, "Mensaje opcional si la aserción falla"
- Si
condicion
es True → no pasa nada. - Si
condicion
es False →AssertionError
con el mensaje.
def porcentaje(p):
assert 0 <= p <= 100, f"Porcentaje fuera de rango: {p}"
return p / 100
print(porcentaje(25)) # 0.25
print(porcentaje(130)) # AssertionError: Porcentaje fuera de rango: 130
assert
verifica supuestos del desarrollador (contratos internos),no valida entradas de usuario ni controla flujo en producción.
6.2 Activación y desactivación (modo optimizado)
Python elimina las aserciones al ejecutar con optimizaciones (-O
o -OO
), por lo que
no deben tener efectos secundarios.
# Ejecutar con aserciones activas (por defecto)
python app.py
# Ejecutar desactivando aserciones
python -O app.py
- Con
-O
, el bytecode resultante omite las sentenciasassert
. - No pongas código con efectos (llamadas a funciones, I/O) dentro de
assert
.
# ❌ Mal: tiene efectos laterales que desaparecerán con -O
assert registrar_evento("verificando...") is None
6.3 Diferencias: assert
vs excepciones
Aspecto | assert | Excepciones (p.ej., raise ValueError ) |
---|---|---|
Propósito | Verificar supuestos del desarrollador | Manejar errores esperables en tiempo de ejecución |
Ámbito | Desarrollo / pruebas | Usuario final / producción |
Estado en -O | Se elimina | Sigue activo |
Mensajes | Breves e internos | Claros, accionables |
6.4 Patrones útiles con assert
Verificar tipos y formatos en código interno
def totalizar(items):
# items: lista de números (contrato interno)
assert isinstance(items, list), "items debe ser list"
assert all(isinstance(x, (int, float)) for x in items), "Elementos numéricos"
return sum(items)
Comprobar invariantes tras una operación
def retirar(cuenta, importe):
saldo_prev = cuenta.saldo
cuenta.saldo -= importe
# El saldo nunca debe crecer tras retirar
assert cuenta.saldo <= saldo_prev, "Invariante roto: saldo incrementó"
Pre y postcondiciones simples
def normalizar(v):
assert len(v) > 0, "Vector vacío"
s = sum(v)
assert s != 0, "No normalizable (suma 0)"
return [x / s for x in v]
6.5 Anti-patrones (lo que NO debes hacer)
- ❌ Usar
assert
para validar entrada de usuario o datos externos (usaif
+raise
). - ❌ Poner efectos laterales en la condición o el mensaje.
- ❌ Depender de
assert
para lógica de negocio. - ❌ Mensajes crípticos: “falló”, “mal” (no ayudan a depurar).
# ✅ Validación de entrada correcta (siempre activa)
def set_edad(edad: int):
if edad < 0:
raise ValueError("edad debe ser ≥ 0")
# Internamente, puedes reforzar supuestos:
assert isinstance(edad, int)
6.6 assert
y testing
En pytest puedes usar assert
directamente; pytest mejora el mensaje al fallar.
En unittest se prefieren métodos como self.assertEqual
, self.assertTrue
, etc.
pytest (estilo directo)
def test_promedio():
datos = [8, 9, 10]
prom = sum(datos) / len(datos)
assert prom == 9
unittest
import unittest
class TestOps(unittest.TestCase):
def test_suma(self):
self.assertEqual(2 + 2, 4)
if __name__ == "__main__":
unittest.main()
6.7 Mensajes efectivos y trazas útiles
Incluye el dato conflictivo y el contexto mínimo para entender el fallo rápido:
def porcentaje(valor):
assert 0 <= valor <= 1, f"valor={valor} fuera de [0,1]"
return valor * 100
6.8 __debug__
y aserciones
La constante __debug__
es True
por defecto y pasa a False
con -O
.
Úsala si necesitas proteger código de depuración adicional.
if __debug__:
# bloque solo en modo no optimizado
verificar_estado = True
6.9 Mini-quiz
- ¿Qué excepción lanza un
assert
fallido? - ¿Por qué no debes usar
assert
para validar datos de usuario? - ¿Qué ocurre con las aserciones al ejecutar
python -O
?
💡 Ver respuestas
- 1️⃣
AssertionError
. - 2️⃣ Porque pueden desactivarse y no son para control de flujo en producción.
- 3️⃣ Se eliminan del bytecode (no se evalúan).
6.10 Prácticas guiadas
Ejercicio 1 — Invariantes de lista
def media(valores):
assert isinstance(valores, list), "valores debe ser list"
assert len(valores) > 0, "lista vacía"
assert all(isinstance(x, (int, float)) for x in valores), "elementos no numéricos"
return sum(valores) / len(valores)
Ejercicio 2 — Postcondición simple
def ordenar(nums):
nums_ordenados = sorted(nums)
# Postcondición: está no decreciente
assert all(nums_ordenados[i] <= nums_ordenados[i+1] for i in range(len(nums_ordenados)-1))
return nums_ordenados
Ejercicio 3 — Contrato interno + excepción pública
def dividir(a, b):
if b == 0:
raise ZeroDivisionError("b no puede ser 0") # público/producción
# refuerzo interno
assert isinstance(a, (int, float)) and isinstance(b, (int, float)), "tipos inválidos"
return a / b
Ejercicio 4 — Sin efectos laterales
Refactoriza esta línea para evitar efectos en assert
:
# Original (mal)
assert log(f"longitud={len(data)}") is None and len(data) > 0
# Solución (bien)
if __debug__:
log(f"longitud={len(data)}")
assert len(data) > 0, "data vacía"
Cierre: usa assert
como red de seguridad durante el desarrollo,
y excepciones explícitas para tratar errores reales de ejecución.
Parte 7 — Buenas prácticas y ejercicios integradores
En esta última sección del módulo aprenderás a combinar todo lo visto: detección, captura,
lanzamiento y validación de errores en un mismo flujo de trabajo.
Además, repasaremos las mejores prácticas para escribir código robusto, limpio y mantenible.
7.1 Principios generales
- ✅ Anticipa los errores más comunes (entradas, archivos, red).
- ✅ Captura solo lo necesario; evita los “try/except” que engullen todo.
- ✅ Usa tipos de excepción específicos (
ValueError
,TypeError
, etc.). - ✅ No uses excepciones como flujo lógico — para eso están los condicionales.
- ✅ Usa
finally
para liberar recursos. - ✅ Documenta las excepciones que puede lanzar una función.
- ✅ Combina
assert
yraise
: el primero para desarrollo, el segundo para producción. - ✅ Encadena errores con
from
cuando traduces excepciones de bajo nivel.
7.2 Patrón de manejo completo
Este patrón agrupa los elementos principales del manejo de errores en Python:
try
, except
, else
, finally
y raise
.
def procesar_dato(valor: str):
try:
numero = int(valor)
except ValueError:
raise ValueError(f"Entrada inválida: '{valor}' no es un número.")
else:
print(f"El número {numero} es válido.")
return numero
finally:
print("Proceso finalizado.")
Salida:
El número 5 es válido.
Proceso finalizado.
7.3 Flujo de control profesional
Un programa bien diseñado separa la validación, el manejo de excepciones
y la lógica principal para mantener claridad y coherencia.
def cargar_archivo(ruta: str) -> str:
"""Lee un archivo y devuelve su contenido."""
if not ruta.endswith(".txt"):
raise ValueError("Solo se admiten archivos .txt")
try:
with open(ruta, "r", encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
raise FileNotFoundError(f"Archivo no encontrado: {ruta}")
except PermissionError:
raise PermissionError(f"Permiso denegado al acceder a: {ruta}")
finally:
print("[LOG] Intento de lectura finalizado.")
7.4 Ejemplo integral 1 — Validación, lanzamiento y captura
class AplicacionError(Exception): ...
class DatoInvalido(AplicacionError): ...
class DivisionInvalida(AplicacionError): ...
def dividir_seguro(a, b):
try:
if not all(isinstance(x, (int, float)) for x in (a, b)):
raise DatoInvalido("Solo se admiten números.")
if b == 0:
raise DivisionInvalida("No se puede dividir entre cero.")
resultado = a / b
except AplicacionError as e:
print("⚠️ Error de aplicación:", e)
else:
print("Resultado:", resultado)
finally:
print("Operación completada.")
Salida esperada:
⚠️ Error de aplicación: No se puede dividir entre cero.
Operación completada.
7.5 Ejemplo integral 2 — Captura jerárquica
class BancoError(Exception): ...
class CuentaInexistente(BancoError): ...
class SaldoInsuficiente(BancoError): ...
def retirar(cuentas, id_cuenta, cantidad):
if id_cuenta not in cuentas:
raise CuentaInexistente("Cuenta no encontrada.")
if cuentas[id_cuenta] < cantidad:
raise SaldoInsuficiente("Saldo insuficiente.")
cuentas[id_cuenta] -= cantidad
return cuentas[id_cuenta]
cuentas = {"A1": 500}
try:
retirar(cuentas, "A1", 800)
except SaldoInsuficiente as e:
print("Error específico:", e)
except BancoError:
print("Error bancario general.")
Salida:
Error específico: Saldo insuficiente.
7.6 Ejemplo integral 3 — Uso conjunto de assert
y raise
def procesar_lista(datos):
assert isinstance(datos, list), "datos debe ser una lista"
if not datos:
raise ValueError("La lista no puede estar vacía.")
if not all(isinstance(x, (int, float)) for x in datos):
raise TypeError("La lista contiene elementos no numéricos.")
promedio = sum(datos) / len(datos)
assert promedio >= 0, "El promedio no puede ser negativo (lógica interna)"
return promedio
7.7 Ejemplo integral 4 — Registro y encadenamiento
class ConfigError(Exception): ...
def cargar_config(ruta):
try:
with open(ruta) as f:
return f.read()
except FileNotFoundError as e:
print("[LOG] Archivo de configuración faltante:", ruta)
raise ConfigError("Error al cargar configuración") from e
try:
cargar_config("conf.ini")
except ConfigError as e:
print("⚠️ Error de configuración detectado:", e.__cause__)
Salida:
[LOG] Archivo de configuración faltante: conf.ini
⚠️ Error de configuración detectado: [Errno 2] No such file or directory: 'conf.ini'
7.8 Ejemplo integral 5 — Manejo de errores en flujo de archivos
def copiar_archivo(origen, destino):
try:
with open(origen, "r") as src:
contenido = src.read()
with open(destino, "w") as dst:
dst.write(contenido)
except FileNotFoundError:
print(f"Archivo no encontrado: {origen}")
except PermissionError:
print(f"Sin permisos para escribir en {destino}")
else:
print("Archivo copiado correctamente.")
finally:
print("Proceso completado.")
7.9 Patrón de registro (logging) recomendado
En lugar de usar solo print()
, usa el módulo logging
para registrar errores de forma profesional.
import logging
logging.basicConfig(level=logging.INFO, filename="app.log", filemode="a")
def conectar_servidor():
try:
raise TimeoutError("Tiempo de espera agotado.")
except TimeoutError as e:
logging.error("Error de conexión: %s", e)
raise
📁 Esto guarda los errores en app.log
con fecha, hora y nivel de severidad.
7.10 Buenas prácticas finales
✅ Hacer | 🚫 Evitar |
---|---|
Usar try/except en operaciones frágiles (I/O, red, parseo) | Encerrar todo el programa en un único try |
Crear excepciones personalizadas por dominio | Lanzar Exception genérica |
Registrar errores con logging | Ignorar las excepciones con pass |
Usar finally o with para limpiar recursos | Olvidar cerrar archivos o conexiones |
Combinar assert en desarrollo y raise en producción | Usar assert para validar entradas de usuario |
7.11 Mini-quiz final
- ¿Qué bloque se ejecuta siempre, haya o no error?
- ¿Qué palabra clave usas para lanzar tus propias excepciones?
- ¿Qué ventaja tiene usar
logging
frente aprint()
? - ¿Qué instrucción se desactiva en modo optimizado (
-O
)?
💡 Ver respuestas
- 1️⃣
finally
. - 2️⃣
raise
. - 3️⃣ Guarda los registros en archivos, con niveles de severidad y marcas de tiempo.
- 4️⃣
assert
.
7.12 Prácticas guiadas finales
Ejercicio 1 — Sistema de reservas
class ReservaError(Exception): ...
class AsientoNoDisponible(ReservaError): ...
class PagoError(ReservaError): ...
def reservar(asiento, ocupado, pago_valido):
if ocupado:
raise AsientoNoDisponible("El asiento ya está ocupado.")
if not pago_valido:
raise PagoError("Error en el pago.")
print("✅ Reserva completada.")
Ejercicio 2 — Cálculo con depuración
def calcular_promedio(notas):
assert isinstance(notas, list)
assert len(notas) > 0, "Lista vacía"
if not all(0 <= n <= 10 for n in notas):
raise ValueError("Notas fuera de rango (0-10)")
return sum(notas) / len(notas)
Ejercicio 3 — Control de acceso
class AccesoError(Exception): ...
def autenticar(usuario, clave):
if usuario != "admin" or clave != "1234":
raise AccesoError("Credenciales inválidas")
print("Bienvenido, administrador.")
try:
autenticar("user", "pass")
except AccesoError as e:
print("❌", e)
else:
print("✅ Autenticación correcta")
Ejercicio 4 — Registro de errores en archivo
import logging
logging.basicConfig(filename="errores.log", level=logging.ERROR)
try:
1 / 0
except ZeroDivisionError as e:
logging.error("Fallo en operación: %s", e)
print("Error registrado en 'errores.log'")
7.13 Conclusión del módulo 3.2
Has aprendido a:
- ✅ Identificar y clasificar los errores comunes en Python.
- ✅ Usar los bloques
try
,except
,else
yfinally
. - ✅ Lanzar y encadenar excepciones con
raise
yfrom
. - ✅ Crear jerarquías de excepciones personalizadas.
- ✅ Utilizar
assert
como herramienta de depuración. - ✅ Escribir código limpio, seguro y preparado para el error.