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

  1. Qué es una excepción: el mecanismo, no el desastre
  2. Anatomía de un traceback: leerlo de abajo arriba
  3. Errores de sintaxis vs excepciones en tiempo de ejecución
  4. Las excepciones integradas que Papyrus ya conoce (sin saberlo)
  5. La jerarquía de excepciones: Exception como madre
  6. 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 clase Libro, un ValueError("el precio no puede ser negativo") es una instancia de la clase ValueError. 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 ejecuta

Cuando 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.

  1. Última línea: el tipo de excepción (FileNotFoundError) y su mensaje. Es el qué. Léela siempre primero.
  2. Penúltima entrada (fichero + línea + código): el lugar exacto donde nació la excepción. Es el dónde.
  3. De ahí hacia arriba: la cadena de llamadas que llevó hasta allí (<module> llamó a cargar_catalogo, que ejecutó el open). Es el cómo se llegó. most recent call last significa 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:

# SyntaxError: Python ni siquiera empieza a ejecutar
if precio > 0
    print("precio válido")
  File "caja.py", line 1
    if precio > 0
                 ^
SyntaxError: expected ':'
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):

  • Exception es la madre de los errores "normales": los que un programa puede razonablemente manejar.
  • BaseException está por encima e incluye cosas que casi nunca debes atrapar, como KeyboardInterrupt (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: KeyError e IndexError son ambas LookupError ("busqué algo y no estaba"); FileNotFoundError es un OSError (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 Exception exactamente igual que ValueError.

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:

  • LBYLLook 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.
  • EAFPEasier 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 SyntaxError con 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 ' y KeyError: '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ón strip().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

  1. 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 de catalogo["hamlet"].precio; (c) ventas = [] seguido de ventas[0]; (d) "Fausto" + 21.00. Después compruébalo en el intérprete y compara el mensaje real con tu predicción.

  2. 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'
    
  3. 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.json al arrancar; (b) comprobar que unidades es mayor que 0 antes de vender; (c) acceder a catalogo[clave] con una clave tecleada por Júlia; (d) comprobar que datos/ existe antes de crearla con mkdir.

Soluciones

  1. (a) ValueError: el tipo (str) es aceptable para int(), 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 concatenar str con float; el mensaje real dice can only concatenate str (not "float") to str.
  2. Qué: un ValueError porque float() recibió 'doce con cincuenta', que no es convertible a número. Dónde: línea 22, dentro de cierre_de_caja. Cadena: el programa principal (<module>, línea 31) llamó a cierre_de_caja(), que en su línea 22 intentó convertir el campo importe de una fila del CSV. Dato culpable: alguien escribió el importe con letras en ventas.csv — exactamente el escenario "reza para que el CSV traiga números" del módulo 6.
  3. (a) EAFP: es estado del mundo exterior; intentar abrir y manejar FileNotFoundError evita la condición de carrera y cubre más fallos que exists(). (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 es raise. (c) EAFP: try con acceso directo (o .get(), que vimos en M4) es más pythónico que if clave in catalogo seguido 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

Módulo 2: Estructuras de Control

Módulo 3: Funciones y Módulos

Módulo 4: Estructuras de Datos

Módulo 5: Programación Orientada a Objetos

Módulo 6: Manejo de Archivos

Módulo 7: Manejo de Errores y Excepciones

Módulo 8: Temas Avanzados

Módulo 9: Pruebas y Depuración

Módulo 10: Desarrollo Web con Python

Módulo 11: Ciencia de Datos con Python

Módulo 12: Proyecto Final

© Copyright 2026. Todos los derechos reservados