El módulo 6 terminó con una lista incómoda de "paliativos": Path.exists() antes de leer, "no edites mal el JSON", "reza para que el CSV traiga números". Papyrus ya tiene memoria — catálogo, ventas, socios y copias de seguridad viven en datos/ — pero esa memoria es frágil: si catalogo.json no está donde debería, si una fila del CSV trae "gratis" en la columna de precio, o si Júlia teclea un número imposible, el programa muere con un bloque de texto rojo a media pantalla. Ese bloque de texto tiene nombre (traceback), ese "morir" tiene mecánica (una excepción que nadie atrapa), y este módulo entero trata de dominar ambos. En esta primera lección no vamos a arreglar nada todavía: vamos a entender qué es exactamente una excepción, a leer un traceback como quien lee un informe de incidencias, a conocer las excepciones integradas que Papyrus ya ha sufrido sin saber su nombre, y a asumir la filosofía que Python prefiere: pedir perdón en vez de pedir permiso.
Contenido
- Qué es una excepción: el mecanismo, no el desastre
- Anatomía de un traceback: leerlo de abajo arriba
- Errores de sintaxis vs excepciones en tiempo de ejecución
- Las excepciones integradas que Papyrus ya conoce (sin saberlo)
- La jerarquía de excepciones:
Exceptioncomo madre - EAFP vs LBYL: por qué
Path.exists()era un paliativo
Qué es una excepción: el mecanismo, no el desastre
Una excepción es un objeto que Python crea cuando una operación no puede completarse, y que interrumpe el flujo normal del programa para viajar hacia arriba buscando a alguien que se haga cargo. Tres ideas clave:
- Es un objeto. Igual que
Libro("Hamlet", 9.95, 6)es una instancia de la claseLibro, unValueError("el precio no puede ser negativo")es una instancia de la claseValueError. Lleva dentro un mensaje y, como veremos en 07-04, puede llevar atributos propios. - Se propaga. Cuando ocurre, la ejecución abandona la línea actual, sale de la función, sale de la función que la llamó, y así sucesivamente. Este viaje ascendente se llama propagación y es lo que hace que un error en la línea 3 de una función auxiliar acabe apareciendo como fallo de todo el programa.
- Si nadie la atrapa, el programa termina. Python imprime el traceback y devuelve el control al sistema operativo. Eso es lo que Ana ha estado viendo hasta ahora: excepciones no manejadas.
Fíjate en el matiz: la excepción no es el problema. La excepción es el aviso estructurado de que hubo un problema. El desastre es no tener a nadie escuchando el aviso.
# ficha_libro.py — el mismo código inocente del módulo 1
precio_texto = input("Precio del libro: ") # Júlia teclea: gratis
precio = float(precio_texto) # aquí nace la excepción
print(f"Con IVA: {precio * 1.04:.2f} €") # esta línea nunca se ejecutaCuando float("gratis") no puede producir un número, Python construye un objeto ValueError, abandona la línea 3 y, como nadie lo atrapa, el programa termina. La línea del print jamás llega a ejecutarse: la propagación corta el flujo en seco.
Anatomía de un traceback: leerlo de abajo arriba
Esto imprime Python al morir el ejemplo anterior:
Traceback (most recent call last):
File "ficha_libro.py", line 2, in <module>
precio = float(precio_texto)
ValueError: could not convert string to float: 'gratis'Y este es un caso real con varias funciones encadenadas, al estilo de caja.py del módulo 6:
Traceback (most recent call last):
File "caja.py", line 42, in <module>
catalogo = cargar_catalogo()
File "caja.py", line 18, in cargar_catalogo
with open(BASE / "datos" / "catalogo.json", encoding="utf-8") as f:
FileNotFoundError: [Errno 2] No such file or directory: 'datos/catalogo.json'La regla de oro para leerlo: empieza por abajo.
- Última línea: el tipo de excepción (
FileNotFoundError) y su mensaje. Es el qué. Léela siempre primero. - Penúltima entrada (fichero + línea + código): el lugar exacto donde nació la excepción. Es el dónde.
- De ahí hacia arriba: la cadena de llamadas que llevó hasta allí (
<module>llamó acargar_catalogo, que ejecutó elopen). Es el cómo se llegó.most recent call lastsignifica literalmente eso: la llamada más reciente está al final.
| Parte del traceback | Pregunta que responde | En el ejemplo |
|---|---|---|
Última línea (Tipo: mensaje) |
¿Qué falló? | No existe datos/catalogo.json |
Última entrada File ... line ... |
¿Dónde nació? | Línea 18, dentro de cargar_catalogo |
| Entradas superiores | ¿Quién llamó a quién? | El programa principal (línea 42) llamó a cargar_catalogo |
Un traceback no es un castigo: es el mejor informe de incidencias gratuito que existe. Aprender a leerlo con calma te ahorrará horas; copiarlo entero (última línea incluida) es también la forma correcta de buscar ayuda.
Errores de sintaxis vs excepciones en tiempo de ejecución
No todo texto rojo es igual. Hay dos familias radicalmente distintas:
Error de sintaxis (SyntaxError) |
Excepción en ejecución | |
|---|---|---|
| Cuándo aparece | Antes de ejecutar nada: al compilar el fichero | En plena ejecución, al llegar a la línea problemática |
| Depende de los datos | No: falla siempre, con cualquier entrada | Sí: float(texto) funciona con "12.50" y explota con "gratis" |
| ¿Se puede manejar con el mecanismo de este módulo? | No: se corrige el código y punto | Sí: es exactamente para lo que sirven try/except (07-02) |
| Ejemplo Papyrus | Olvidar los dos puntos de un if |
El CSV de ventas trae una fila corrupta |
La consecuencia práctica es importante: un SyntaxError es siempre un bug tuyo y se arregla editando; una excepción en ejecución puede ser un bug o puede ser el mundo real comportándose como el mundo real (archivos que faltan, usuarios que teclean cualquier cosa). Este módulo trata la segunda familia.
Las excepciones integradas que Papyrus ya conoce (sin saberlo)
Python trae decenas de excepciones predefinidas. Estas siete cubren el 90 % del día a día, y todas tienen ya un episodio en la historia de Papyrus:
| Excepción | Cuándo salta | Ejemplo Papyrus |
|---|---|---|
ValueError |
El tipo es correcto pero el valor no vale | float("gratis") al leer ventas.csv; el __post_init__ de Libro con precio=-5 (M5) |
TypeError |
El tipo de dato no es el esperado para la operación | "12.50" * 0.04 no falla (repite texto), pero "12.50" + 0.04 sí: no se suma str con float |
KeyError |
Clave inexistente en un diccionario | catalogo["fausto "] — el espacio final rompe la clave normalizada con strip().casefold() (M4) |
IndexError |
Índice fuera de rango en lista o tupla | ultimas_ventas[10] cuando hoy solo hubo 3 ventas |
FileNotFoundError |
Se intenta abrir un archivo que no existe | open(BASE / "datos" / "catalogo.json") en el portátil nuevo de Ana, donde datos/ aún no existe (M6) |
ZeroDivisionError |
División entre cero | total_ventas / numero_ventas en cierre_de_caja() un día sin ventas |
AttributeError |
El objeto no tiene ese atributo o método | libro.titolo (errata) en vez de libro.titulo; o llamar .append() sobre el dict del catálogo |
Merece la pena provocarlas a propósito en el intérprete interactivo: teclea int("Hamlet"), {}["fausto"], [][0]... Ver el nombre y el mensaje de cada una en un entorno controlado hace que las reconozcas al instante cuando aparezcan en un traceback real.
>>> catalogo = {"hamlet": "Libro(Hamlet, 9.95, 6)"}
>>> catalogo["Hamlet "] # olvidamos normalizar la clave
Traceback (most recent call last):
...
KeyError: 'Hamlet 'El mensaje del KeyError muestra la clave buscada con sus comillas: ahí se ve el espacio traidor. Otro motivo para leer la última línea con lupa.
La jerarquía de excepciones: Exception como madre
Las excepciones son clases, y como toda clase de Python (lo vimos en el módulo 5) pueden heredar. Casi todas las que te interesan descienden de Exception:
graph TD
BE[BaseException] --> KI[KeyboardInterrupt<br/>Ctrl+C]
BE --> EX[Exception]
EX --> AE[ArithmeticError] --> ZD[ZeroDivisionError]
EX --> LE[LookupError] --> KE[KeyError]
LE --> IE[IndexError]
EX --> VE[ValueError]
EX --> TE[TypeError]
EX --> ATE[AttributeError]
EX --> OS[OSError] --> FNF[FileNotFoundError]
Ideas que conviene retener ya (les sacaremos partido en 07-02 y 07-04):
Exceptiones la madre de los errores "normales": los que un programa puede razonablemente manejar.BaseExceptionestá por encima e incluye cosas que casi nunca debes atrapar, comoKeyboardInterrupt(el Ctrl+C con el que Ana corta el programa a propósito). Por eso "atraparlo todo" es mala idea: podrías impedir hasta que el usuario cancele.- Las categorías intermedias agrupan:
KeyErroreIndexErrorson ambasLookupError("busqué algo y no estaba");FileNotFoundErrores unOSError(problema con el sistema operativo). Manejar la categoría maneja a todas sus hijas — el mismo polimorfismo del módulo 5, aplicado a errores. - En 07-04 extenderemos este árbol con excepciones propias de Papyrus, colgando de
Exceptionexactamente igual queValueError.
EAFP vs LBYL: por qué Path.exists() era un paliativo
El módulo 6 lo llamó paliativo una y otra vez. Ahora podemos explicar por qué con vocabulario técnico. Hay dos filosofías para tratar con operaciones que pueden fallar:
- LBYL — Look Before You Leap ("mira antes de saltar"): comprobar las precondiciones antes de actuar.
if ruta.exists(): open(ruta). Es lo que hicimos en el módulo 6. - EAFP — Easier to Ask Forgiveness than Permission ("es más fácil pedir perdón que permiso"): actuar directamente y manejar la excepción si algo sale mal. Es el estilo idiomático de Python.
| Criterio | LBYL (if ruta.exists():) |
EAFP (intentar y manejar) |
|---|---|---|
| Condición de carrera | Entre el exists() y el open() pasa tiempo: otro proceso (¡la copia nocturna!) puede borrar o mover el archivo en medio. La comprobación no garantiza nada |
El open() es la propia comprobación: o funciona o avisa, sin hueco intermedio |
| Doble trabajo | El sistema busca el archivo dos veces: una para exists(), otra para open() |
Una sola operación |
| Cobertura | exists() solo cubre "no existe". ¿Y si existe pero está corrupto, o sin permisos de lectura, o es un directorio? Necesitarías un if por cada caso |
La excepción cubre cualquier fallo, incluso los que no anticipaste |
| Legibilidad | El camino feliz queda enterrado entre comprobaciones | El camino feliz se lee limpio; el manejo de errores va aparte |
La condición de carrera (race condition) merece un ejemplo concreto: la copia nocturna de Papyrus (copia_de_seguridad(), M6) renombra y mueve archivos en datos/. Si caja.py comprueba catalogo_json.exists() → True, y una décima de segundo después el proceso de copia mueve ese archivo, el open() posterior explota a pesar de la comprobación. LBYL da una falsa sensación de seguridad: verificaste una foto del pasado, no el presente.
Ojo: EAFP no significa "no valides nunca". Validar datos de entrada (¿es negativo este precio que tecleó Júlia?) sigue siendo correcto y lo refinaremos en 07-03. Lo que EAFP desaconseja es intentar predecir el estado del mundo exterior (archivos, red, otros procesos) con comprobaciones previas, cuando el propio intento ya te dice la verdad.
Lo que aún no sabemos hacer es "pedir perdón": manejar la excepción cuando llega. Ese es exactamente el tema de la próxima lección.
Errores Comunes y Consejos
- Leer el traceback de arriba abajo y asustarse a la segunda línea. Al revés: última línea primero (tipo y mensaje), luego el punto de origen, luego la cadena de llamadas. De abajo arriba, siempre.
- Confundir
SyntaxErrorcon una excepción manejable. Si el error aparece antes de que se ejecute nada, es sintaxis: se corrige el código, no se "maneja". - Pensar que
KeyError: 'Hamlet 'yKeyError: 'hamlet'son el mismo error. El mensaje muestra la clave exacta entre comillas: espacios y mayúsculas incluidos. En Papyrus, casi siempre significa que alguien saltó la normalizaciónstrip().casefold(). - Creer que
Path.exists()"arregla" el problema del archivo ausente. Solo cubre un fallo entre muchos y deja una ventana temporal abierta. Es un paliativo — el módulo 6 lo dijo, y ahora sabes por qué. - Consejo: cuando un traceback te desconcierte, reproduce la excepción a mano en el intérprete con datos mínimos (
float("gratis"),{}["x"]). Aislar el fallo del programa grande es la mitad del diagnóstico. - Consejo: guarda los tracebacks que te encuentres estos días. En 07-05 aprenderás a enviarlos automáticamente a un archivo de log en vez de perderlos al cerrar la terminal.
Ejercicios
-
Detective de tracebacks. Sin ejecutar nada, predice qué excepción (tipo exacto) lanza cada fragmento y por qué: (a)
int("12.50"); (b)catalogo = {}seguido decatalogo["hamlet"].precio; (c)ventas = []seguido deventas[0]; (d)"Fausto" + 21.00. Después compruébalo en el intérprete y compara el mensaje real con tu predicción. -
Autopsia de un traceback. Dado este traceback, responde: ¿qué falló?, ¿en qué línea y función nació?, ¿cuál fue la cadena de llamadas completa?, ¿qué dato provocó el fallo?
Traceback (most recent call last): File "caja.py", line 31, in <module> total = cierre_de_caja() File "caja.py", line 22, in cierre_de_caja importes.append(float(fila["importe"])) ValueError: could not convert string to float: 'doce con cincuenta' -
Clasificar filosofías. Para cada situación de Papyrus, decide si encaja mejor LBYL o EAFP y justifica en una frase: (a) abrir
datos/config.jsonal arrancar; (b) comprobar queunidadeses mayor que 0 antes de vender; (c) acceder acatalogo[clave]con una clave tecleada por Júlia; (d) comprobar quedatos/existe antes de crearla conmkdir.
Soluciones
- (a)
ValueError: el tipo (str) es aceptable paraint(), pero el valor"12.50"no representa un entero —int()no trunca cadenas decimales. (b)KeyError: 'hamlet': el diccionario está vacío; nótese que el fallo salta en el acceso[...], antes de llegar siquiera a.precio. (c)IndexError: lista vacía, no hay posición 0. (d)TypeError: no se puede concatenarstrconfloat; el mensaje real dicecan only concatenate str (not "float") to str. - Qué: un
ValueErrorporquefloat()recibió'doce con cincuenta', que no es convertible a número. Dónde: línea 22, dentro decierre_de_caja. Cadena: el programa principal (<module>, línea 31) llamó acierre_de_caja(), que en su línea 22 intentó convertir el campoimportede una fila del CSV. Dato culpable: alguien escribió el importe con letras enventas.csv— exactamente el escenario "reza para que el CSV traiga números" del módulo 6. - (a) EAFP: es estado del mundo exterior; intentar abrir y manejar
FileNotFoundErrorevita la condición de carrera y cubre más fallos queexists(). (b) LBYL (validación de entrada): es un dato que ya tienes en la mano; comprobarlo antes es barato, sin ventana temporal — en 07-03 veremos que la reacción correcta a esa comprobación esraise. (c) EAFP:trycon acceso directo (o.get(), que vimos en M4) es más pythónico queif clave in catalogoseguido del acceso — dos búsquedas en vez de una. (d) Truco: ni lo uno ni lo otro —ruta.mkdir(exist_ok=True)(M6) elimina el problema de raíz; la mejor comprobación es la que no hace falta.
Conclusión
Ya tienes el mapa del territorio: una excepción es un objeto que interrumpe el flujo y se propaga hacia arriba hasta que alguien la atrapa o el programa muere; el traceback es su informe forense y se lee de abajo arriba; los errores de sintaxis son otra especie (se corrigen, no se manejan); las excepciones integradas forman una jerarquía de clases con Exception como madre, y Python prefiere EAFP — intentar y pedir perdón — frente al LBYL de Path.exists(), que deja condiciones de carrera abiertas y solo cubre los fallos que supiste anticipar. Pero de momento seguimos igual que Ana: vemos venir el golpe y no sabemos encajarlo. La siguiente lección introduce la herramienta central de todo el módulo, try/except, y con ella saldaremos dos deudas históricas: cargar_catalogo() dejará de depender de que catalogo.json exista, y el int(input()) que arrastra su ValueError desde el módulo 1 tendrá por fin una validación digna.
Curso de Programación en Python
Módulo 1: Introducción a Python
- Introducción a Python
- Configuración del Entorno de Desarrollo
- Sintaxis de Python y Tipos de Datos Básicos
- Variables y Constantes
- Entrada y Salida Básica
- Entornos Virtuales y Gestión de Paquetes
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for y while
- Herramientas de Control de Flujo
- Comprensiones de Listas
Módulo 3: Funciones y Módulos
- Definición de Funciones
- Argumentos de Función
- Funciones Lambda
- Módulos y Paquetes
- Visión General de la Biblioteca Estándar
Módulo 4: Estructuras de Datos
Módulo 5: Programación Orientada a Objetos
Módulo 6: Manejo de Archivos
- Lectura y Escritura de Archivos
- Trabajo con Archivos CSV
- Manejo de Datos JSON
- Operaciones con Archivos y Directorios
Módulo 7: Manejo de Errores y Excepciones
- Introducción a las Excepciones
- Manejo de Excepciones
- Lanzamiento de Excepciones
- Excepciones Personalizadas
- Buenas Prácticas y Registro de Errores con logging
Módulo 8: Temas Avanzados
- Anotaciones de Tipos (type hints)
- Decoradores
- Generadores
- Administradores de Contexto
- Concurrencia: Hilos y Procesos
- Asyncio para Programación Asíncrona
Módulo 9: Pruebas y Depuración
- Introducción a las Pruebas
- Pruebas Unitarias con unittest
- Pruebas con pytest
- Desarrollo Guiado por Pruebas
- Técnicas de Depuración
- Uso de pdb para Depuración
Módulo 10: Desarrollo Web con Python
- Introducción al Desarrollo Web
- Fundamentos del Framework Flask
- Construcción de APIs REST con Flask
- Introducción a Django
- Construcción de Aplicaciones Web con Django
Módulo 11: Ciencia de Datos con Python
- Introducción a la Ciencia de Datos
- NumPy para Computación Numérica
- Pandas para Manipulación de Datos
- Matplotlib para Visualización de Datos
- Introducción al Aprendizaje Automático con scikit-learn
