Cuando escribes except ValueError, no estás capturando un error cualquiera: estás filtrando por posición exacta dentro de un árbol de clases que Python construyó con intención. Conocer ese árbol cambia completamente cómo diseñas el manejo de errores.
En la cima está BaseException, la clase raíz de todo lo que se puede lanzar con raise. Pero casi nadie debería capturar BaseException directamente, porque de ella cuelgan tres excepciones que existen precisamente para no ser interceptadas por código de usuario: SystemExit (lanzada por sys.exit()), KeyboardInterrupt (Ctrl+C) y GeneratorExit (cuando se cierra un generador o coroutine). Si pusieras except BaseException, atraparías también esas tres, impidiendo que el proceso termine limpiamente o que el usuario pueda interrumpir un script colgado.
Por eso existe Exception: es la subclase de BaseException que agrupa todo lo que sí es un error de programa — ValueError, TypeError, RuntimeError, y miles más — pero deja fuera a SystemExit, KeyboardInterrupt y GeneratorExit. Cuando escribes except Exception:, capturas una cantidad enorme de errores sin tocar los mecanismos de control del ciclo de vida del proceso. Es el límite correcto para un except genérico de último recurso.
Lo interesante del diseño es que la jerarquía agrupa errores por categoría semántica, no por módulo de origen. LookupError es la base compartida de KeyError (acceso a diccionario) e IndexError (acceso a lista/tupla). Si escribes except LookupError:, estás diciendo “no me importa si fue un índice inválido o una clave ausente, en ambos casos no encontré lo que buscaba”. Lo mismo con OSError: FileNotFoundError, PermissionError y TimeoutError son subclases suyas, lo que te permite capturar todos los errores de sistema de archivos y red con un solo except OSError: cuando la distinción no importa, o ser quirúrgico con except FileNotFoundError: cuando sí importa.
Si capturas un error más general del que necesitas, pierdes información y potencialmente silencias fallos distintos al que esperabas manejar. Si capturas uno demasiado específico, tu código explota ante variantes perfectamente válidas del mismo problema.
import sys
import errno
def read_config(path: str) -> str:
"""Lee un archivo de configuración con manejo estratificado de errores."""
try:
with open(path, encoding="utf-8") as f:
return f.read()
except FileNotFoundError:
# Caso específico: podemos crear un config por defecto
print(f"Config not found at {path!r}, using defaults.")
return ""
except PermissionError:
# Sabemos exactamente qué falló; relanzamos con contexto
raise PermissionError(
f"Cannot read {path!r}: check file permissions."
) from None
except OSError as exc:
# Captura cualquier otro error de I/O (disco lleno, dispositivo no disponible, etc.)
# OSError.errno da el código POSIX cuando está disponible
if exc.errno == errno.ENOSPC:
raise RuntimeError("Disk full, cannot read config.") from exc
raise # Para OSError desconocidos, relanzamos sin modificar
def process_data(data: dict, keys: list[str]) -> list:
"""Extrae valores de un dict; LookupError cubre KeyError e IndexError."""
results = []
for key in keys:
try:
results.append(data[key])
except LookupError as exc:
# No importa si fue KeyError (dict) o IndexError (si data fuera una lista)
# LookupError nos da flexibilidad sin perder el tipo real en exc
print(f"Key not found: {exc!r}")
return results
def main() -> None:
config = read_config("settings.toml")
sample = {"host": "localhost", "port": 5432}
values = process_data(sample, ["host", "port", "database"])
print(values)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
# Capturamos explícitamente para dar salida limpia sin traceback
print("\nInterrupted by user.")
sys.exit(130) # Convención POSIX para "terminado por señal"
# SystemExit y GeneratorExit NO están aquí: los dejamos subir libremente
Qué significa cada decisión
La función read_config muestra el patrón de captura estratificada: primero la excepción más específica (FileNotFoundError), luego una hermana (PermissionError), y finalmente la clase base OSError como red de seguridad. Python evalúa los except en orden y usa el primero que coincide, así que poner OSError antes de FileNotFoundError haría que nunca llegaras al caso específico — FileNotFoundError es subclase de OSError, así que el primero ganaría siempre.
Fíjate en except OSError as exc con el chequeo de exc.errno: OSError unifica decenas de errores de sistema, pero a veces necesitas distinguir dentro de ella sin ramificarte en subclases. El atributo errno da acceso al código POSIX cuando existe, y el módulo errno tiene las constantes nombradas.
El raise desnudo al final del bloque OSError relanza la excepción original intacta, preservando el traceback completo. Es muy diferente a raise exc, que reinicia el traceback desde ese punto y oculta el origen real.
En process_data, capturar LookupError en lugar de KeyError no es pereza — es una decisión de diseño que dice “esta función no asume la estructura interna de data“. Si mañana cambias data de dict a algo indexable por entero, el manejo de errores sigue siendo correcto sin tocar process_data.
En main, el except KeyboardInterrupt está separado y explícito. Si hubieras puesto except Exception: en su lugar, Ctrl+C no entraría ahí porque KeyboardInterrupt no hereda de Exception. Eso es exactamente el diseño intencional del árbol: las interrupciones de control de proceso no son errores de programa.
Errores que debes conocer
Error: capturar Exception pensando que intercepta KeyboardInterrupt, y luego preguntarte por qué Ctrl+C no funciona en ciertos puntos.
# ❌ Wrong
while True:
try:
work()
except Exception:
pass # KeyboardInterrupt nunca llega aquí — no hereda de Exception
# ✅ Right
while True:
try:
work()
except Exception:
pass
except KeyboardInterrupt:
print("Shutting down.")
break
KeyboardInterrupt hereda de BaseException directamente, así que necesita su propio bloque except si quieres manejarlo.
Error: poner except OSError: antes de except FileNotFoundError: y preguntarte por qué el caso específico nunca ejecuta.
# ❌ Wrong
try:
open("missing.txt")
except OSError:
print("generic") # siempre entra aquí
except FileNotFoundError:
print("not found") # código muerto
# ✅ Right
try:
open("missing.txt")
except FileNotFoundError:
print("not found") # primero lo específico
except OSError:
print("generic")
Python usa el primer except cuya clase es superclase o igual a la excepción lanzada; como FileNotFoundError es subclase de OSError, el bloque padre siempre gana si va primero.
N° 77