100 Días de Código en Python — Día 30: Errores, excepciones y datos JSON
Nivel: Grado décimo
Área: Tecnología e Informática
Modalidad: Tutorial práctico
Fuente: Curso "100 Days of Code: The Complete Python Pro Bootcamp" (Angela Yu, App Brewery). Contenido del Día 30 (nivel intermedio).
29 min de lectura
15 secciones
Este tutorial adapta pedagógicamente el Día 30. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso. Los retos 🟢🟡🟠🔴, el proyecto extra y el proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.
Nota sobre las verificaciones: los ejemplos se ejecutaron con Python 3.14. El archivo inicial del proyecto OTAN y el código completo del gestor de contraseñas que enlaza el curso no están en la carpeta; este tutorial los reconstruye a partir de las lecciones de los Días 26, 29 y 30.
🔐 Recuerda la advertencia del Día 29: MyPass guarda las contraseñas sin cifrar. Úsalo solo con datos inventados.
(Fuente: 227 - Day 30 Goals- what you will make by the end of the day.md)
¡Día 30! Para celebrar el hito, hoy aprenderemos dos temas que usan los programadores profesionales todos los días:
Errores y excepciones: cómo evitar que un programa se "caiga" cuando algo sale mal.
JSON: probablemente el formato más usado para intercambiar datos entre aplicaciones por internet, y no solo en Python.
Con ambos mejoraremos MyPass: guardará los datos en JSON, no se caerá si el archivo no existe y tendrá una función de búsqueda: escribes un sitio web, pulsas Search y aparecen el correo y la contraseña guardados.
Prerequisito general: los errores que ya conoces (KeyError, IndexError, TypeError, FileNotFoundError), la depuración (Día 13), archivos y modos "r", "w" y "a" (Día 24), diccionarios anidados (Día 9), el proyecto OTAN (Día 26) y MyPass (Día 29).
03
1. Capturar excepciones: try, except, else y finally#
Hasta hoy, un error significaba "hay que arreglar el código". Pero algunos errores no dependen de nosotros: el archivo que el usuario borró, lo que escribió en una entrada, internet que se cayó… La vida sigue la ley de Murphy: "si algo puede salir mal, en algún momento saldrá mal". El curso lo compara con el seguro de un carro: probablemente no se va a varar, pero es mejor tener cobertura por si acaso.
El curso muestra un caso real: si en MyPass se escribe mal el nombre del archivo (daata.txt) y se abre en modo lectura, al pulsar Add aparece FileNotFoundError, y las líneas siguientes no se ejecutan: las entradas no se borran. Un error detiene todo lo que viene después.
Las cuatro palabras clave:
Python
12345678
try:# Algo que PODRÍA fallarexcept:# Qué hacer SI hubo una excepciónelse:# Qué hacer si NO hubo excepciónfinally:# Qué hacer PASE LO QUE PASE
Bloque
Se ejecuta cuando…
Uso típico
try
Siempre se intenta
El código riesgoso
except
Ocurrió una excepción en el try
Un plan B
else
El try terminó sin excepciones
Lo que depende de que el try haya funcionado
finally
Siempre, al final
"Limpiar": cerrar archivos, liberar recursos
El curso dice, con humor, que a finally "no le importa" si todo salió bien o mal: se ejecuta siempre.
🔎 Ejemplo explicado (el ejemplo del curso, construido por partes)#
Paso 1 — Un plan B útil. Imprimir "hubo un error" no sirve de mucho. Si el archivo no existe, lo creamos:
Recuerda: el modo "w"crea el archivo si no existe.
Paso 2 — El problema del except "desnudo". PyCharm advierte: Too broad exception clause. Según la guía de estilo PEP 8, nunca se debe usar un except sin especificar la excepción, porque captura cualquier error, incluso los que no esperabas.
El curso lo demuestra: agrega dentro del try una línea que produce un KeyError. Con el except desnudo, no aparece ningún error, y el programa intenta crear el archivo como si el problema hubiera sido el archivo. ¡El error queda escondido!
Paso 3 — Excepciones específicas, cada una con su plan:
Python
123456789101112131415
try:file=open("a_file.txt")
a_dictionary ={"key":"value"}print(a_dictionary["sdfsdf"])except FileNotFoundError:file=open("a_file.txt","w")file.write("Something")except KeyError as error_message:print(f"The key {error_message} does not exist.")else:
content =file.read()print(content)finally:file.close()print("File was closed.")
except KeyError as error_message guarda el mensaje del error. Resultado verificado: The key 'sdfsdf' does not exist. (con comillas, porque el mensaje de un KeyError es la clave con su representación).
El else solo se ejecuta si todo el try funcionó. La primera vez (sin archivo) no se ejecuta; la siguiente, con el archivo ya creado y una clave válida, imprime Something.
El finally cierra el archivo siempre, porque aquí no usamos with.
🛠️ Nota técnica: este ejemplo funciona porque en todos los caminos se asigna file antes de llegar al finally. Si agregaras una excepción que ocurra antes de abrir el archivo, file.close() fallaría con NameError. Por eso, en programas reales se prefiere with, que cierra el archivo solo. El curso usa open sin with a propósito, para mostrar para qué sirve finally.
🟢 Reto 1: pide al usuario un número entero con input. Si escribe letras, captura el ValueError y muestra un mensaje amable.
🟡 Reto 2: repite la pregunta del Reto 1 hasta que el usuario escriba un número válido. (Pista: while True y break.)
🟠 Reto 3: una calculadora de división que capture por separadoValueError (letras) y ZeroDivisionError (dividir entre cero), con un mensaje distinto para cada uno.
🔴 Reto 4: vuelve al conversor de millas del Día 27 y a la calculadora del Reto 3 del Día 27. Haz que ninguna entrada del usuario produzca un error en la consola: todos los problemas se muestran en la interfaz.
La quinta palabra clave relacionada es raise (lanzar). Con ella provocamos nosotros una excepción, usando cualquiera de las clases conocidas y, si queremos, un mensaje:
Python
1
raise TypeError("This is an error that I made up.")
¿Para qué querría alguien provocar un error? Para los casos en que el código es perfectamente válido, pero produciría un resultado incorrecto.
Ejemplo del curso: el IMC.
Python
12345678
height =float(input("Height: "))# en metros
weight =int(input("Weight: "))# en kilogramosif height >3:raise ValueError("Human Height should not be over 3 meters.")
bmi = weight / height **2print(bmi)
Si alguien escribe 45 m de estatura ("como Godzilla", dice el curso), Python calcula el IMC sin quejarse: todo es válido. Pero el resultado no tiene sentido. Con raise ValueError(...), el programa se detiene antes de dar un resultado falso.
ValueError es la excepción adecuada cuando el tipo del dato es correcto, pero el valor no.
El curso muestra tres formas de elevar al cuadrado: height * height, pow(height, 2) y height ** 2. Recomienda la última.
💡 Conexión:raise y try/except trabajan juntos. Una función puede lanzar un ValueError cuando recibe un dato imposible, y la interfaz que la llama puede capturarlo para mostrar un mensaje en pantalla. Lo usarás en el proyecto integrador.
🟢 Reto 1: en el IMC, lanza también un ValueError si la estatura o el peso son cero o negativos.
🟡 Reto 2: crea registrar_nota(nota) que lance ValueError si la nota no está entre 1.0 y 5.0 (la escala más usada en Colombia).
🟠 Reto 3: llama a registrar_nota dentro de un try/except ValueError as error e imprime el mensaje del error.
🔴 Reto 4: ¿qué rangos razonables tendrían la edad de un estudiante de colegio, la temperatura de un salón o el número de estudiantes de un curso? Escribe una función de validación para cada caso y justifica los límites que elegiste.
05
3. Ejercicio: excepciones en el proyecto de la OTAN#
(Fuente: 230 - Code Exercise- Exception Handling in the NATO Phonetic Alphabet Project.md)
Situación: en el deletreador OTAN del Día 26, si el usuario escribe algo que no está en el diccionario (por ejemplo, 1234), el programa se cae con KeyError.
Enunciado: haz que, si el usuario escribe algo que no sean letras, el programa diga "Sorry, only letters in the alphabet please." y vuelva a pedir una palabra, tantas veces como sea necesario, hasta recibir una palabra válida.
¿Cuál es exactamente la línea que puede fallar? ¿Con qué excepción?
¿Qué debe pasar si no falla?
¿Cómo haces que el programa vuelva a preguntar? (El curso menciona dos opciones: un bucle o una función.)
Solución del curso:
Python
1234567891011
defgenerate_phonetic():
word =input("Enter a word: ").upper()try:
output_list =[phonetic_dict[letter]for letter in word]except KeyError:print("Sorry, only letters in the alphabet please.")
generate_phonetic()else:print(output_list)
generate_phonetic()
Solo la comprensión va en el try, porque es la línea que depende de lo que escribió el usuario.
El else imprime el resultado solo si todo salió bien.
Para volver a preguntar, la función se llama a sí misma dentro del except. Eso se llama recursión.
🛠️ Nota técnica (verificada): la recursión funciona, pero cada respuesta inválida deja una llamada "abierta". Python limita la profundidad a 1000 llamadas por defecto (sys.getrecursionlimit()), así que tras cerca de mil errores seguidos el programa se caería con RecursionError. Nadie se equivoca mil veces en la práctica, pero la forma más habitual de "repetir hasta que sea válido" es un bucle:
Python
123456789
whileTrue:
word =input("Enter a word: ").upper()try:
output_list =[phonetic_dict[letter]for letter in word]except KeyError:print("Sorry, only letters in the alphabet please.")else:print(output_list)break
Las dos soluciones son válidas. La del curso practica funciones; la del bucle no tiene límite de intentos.
🟢 Reto 1: usa except KeyError as error para decirle al usuario qué carácter no es válido. Verificado: con Muñoz, el error es 'Ñ'.
🟡 Reto 2: acepta frases: los espacios no deben producir error. (Pista: ¿filtrar en la comprensión o agregar el espacio al diccionario?)
🟠 Reto 3: decide qué hacer con la Ñ y las tildes (el Día 26 dejó esta pregunta abierta) e impleméntalo.
🔴 Reto 4: ¿este problema se podría resolver con if/else en lugar de excepciones? Escribe las dos versiones y, después de leer la sección 6, argumenta cuál es mejor en este caso.
El problema: buscar un sitio web en data.txt es muy difícil. Es "un formato terrible para trabajar", dice el curso: habría que leer cada línea, separarla por |…
JSON (JavaScript Object Notation; el curso bromea: "no es el DJ del momento") se diseñó para JavaScript, pero es tan simple que lo adoptaron casi todos los lenguajes. Está hecho de diccionarios y listas anidados, con pares clave-valor:
¿Por qué no abrir en modo "a" y agregar al final? Porque dos objetos JSON pegados uno tras otro no son un JSON válido. El curso lo muestra: el archivo se llena de errores.
🛠️ Nota técnica: en la transcripción se habla de un método "JSON dot update". En realidad update es un método de los diccionarios de Python (data.update(new_data)), no del módulo json. También se dice que a json.load "le pasamos la ruta del fichero": en realidad recibe el objeto archivo abierto con open (data_file), no el nombre del archivo como texto.
💡 Un efecto útil de update (verificado en la sección 5): si el sitio web ya existe, updatereemplaza sus datos en lugar de duplicarlos. ¡Esto resuelve el problema de los sitios repetidos que quedó pendiente en el Día 29!
Simplificación del curso: para concentrarse en JSON, la lección quita la ventana de confirmación askokcancel del Día 29. Puedes volver a ponerla cuando todo funcione.
🟢 Reto 1: crea mi_perfil.json con tu nombre (o un alias), tu grado y una lista de tus materias favoritas. Léelo e imprime cada materia.
🟡 Reto 2: escribe un programa que agregue una materia a la lista del Reto 1 sin borrar las anteriores.
🟠 Reto 3: convierte tu mi_semana.csv del Día 25 a JSON con pandas y con el módulo json. Compara los dos archivos: ¿cuál es más fácil de leer?
🔴 Reto 4: investiga qué tipos de Python no se pueden guardar directamente en JSON (prueba con una tupla, un conjunto y una fecha). ¿Qué pasa con cada uno? ¿Cómo los guardarías?
07
5. 🚀 Proyecto del día: MyPass 2.0 con JSON, excepciones y búsqueda#
(Fuentes: 231 - Write, read and update JSON data in the Password Manager.md, 232 - Challenge 1 - Handling Exceptions in the Password Manager.md, 233 - Challenge 2 - Search for a Website in the Password Manager.md)
Convertir MyPass en una aplicación robusta que guarde en JSON, no se caiga la primera vez que se usa y permita buscar las credenciales de un sitio web.
Versión 1 — Guardar en JSON (lección 231). Aplica los tres pasos de la sección 4 dentro de save(), en lugar de escribir en data.txt.
El problema que aparece: si borras data.json y ejecutas el programa, al pulsar Add se produce FileNotFoundError: el paso 1 intenta leer un archivo que aún no existe.
Versión 2 — Manejar la excepción (desafío 1 del curso, lección 232).
Enunciado del curso: sin data.json, el programa no debe caerse, sino crear el archivo con los datos nuevos. Si ya existe, debe leer, actualizar y escribir.
Sobre la repetición: el curso reconoce que escribir el archivo aparece dos veces y que podrías crear una función aparte. Decide dejarlo así para que el try/except/else/finally completo se vea en un solo lugar. Tú puedes refactorizarlo (ver ⭐).
Versión 3 — La interfaz de búsqueda (desafío 2 del curso, lección 233).
Botón Search en la fila 1, columna 2 (la misma columna que Generate Password).
Al principio aparece encima de la entrada del sitio web: esta tenía columnspan=2. Hay que quitarle el columnspan y darle width=21, como la entrada de la contraseña.
Para que Search tenga casi el mismo ancho que Generate, el curso prueba anchos por ensayo y error: 10 es poco; 13 queda casi igual.
Versión 4 — La búsqueda. Crea find_password() (en su propia sección del archivo) y conéctala con command=find_password.
Consejo del curso: primero guarda uno o dos sitios para tener un data.json con el que probar, e imprime el diccionario cargado para ver su forma. Las excepciones se manejan después.
🧩 Tu turno (la lógica, en palabras):
Obtener el sitio web de la entrada.
Abrir data.json y cargar los datos.
Si el sitio web está en los datos: sacar el correo con data[website]["email"] y la contraseña con data[website]["password"] (un diccionario anidado), y mostrarlos con messagebox.showinfo(title=website, message=...).
Versión final — Los dos casos que faltan.
Sin data.json (primera vez): FileNotFoundError → ventana "Error" — "No Data File Found.".
Sitio web no guardado (por ejemplo, "Facebook"): el if da False y no pasa nada. Para el usuario, parece que la aplicación no funciona. Solución del curso: un else con "No details for [sitio] exists.".
Mayúsculas. La búsqueda distingue mayúsculas: "amazon" in {"Amazon": ...} da False (verificado). ¿Normalizas los nombres al guardar, al buscar o en ambos momentos? Justifica tu decisión.
Archivo vacío. La transcripción de la lección 231 menciona el caso en que el archivo existe pero no tiene datos. Si data.json existe pero está vacío (por ejemplo, porque el programa se cerró a mitad de una escritura), json.load lanza JSONDecodeError (verificado) y ninguno de los except del curso lo captura. Agrega el manejo adecuado en save() y en find_password(). ¿Qué debería hacer el programa en cada caso?
finally siempre borra. Con la solución del curso, las entradas se borran incluso si ocurriera un error inesperado al guardar. ¿Es buena idea? ¿Qué pasaría si el usuario pierde lo que escribió sin que se haya guardado?
Validación. Asegúrate de que la validación de campos vacíos del Día 29 (con la mejora de strip()) siga funcionando en la versión JSON.
(Fuente: 233 - Challenge 2 - Search for a Website in the Password Manager.md, parte final)
Muchos estudiantes preguntan: si el sitio web no existe, ¿por qué no lanzar una excepción y capturarla en otra parte?
La regla del curso:
Si puedes resolverlo fácilmente con if/else, usa if/else. Usa try/except/else/finally cuando no tengas una forma fácil de revisar la situación y el error se va a lanzar de todos modos.
Otra forma de pensarlo: una excepción debe ser algo excepcional, algo que ocurre muy rara vez. if/else es para lo que pasa con frecuencia.
Situación en MyPass
¿Frecuente?
¿Hay forma fácil de revisarla antes?
Herramienta
Buscar un sitio que no se guardó
Sí, muy común
Sí: if website in data
if/else
El archivo de datos no existe
Raro (solo la primera vez)
No de forma sencilla con lo que sabemos
try/except FileNotFoundError
💡 Para investigar (adaptación): en realidad sí existe una forma de revisar si un archivo existe antes de abrirlo: Path("data.json").exists() del módulo pathlib, que viste en el Día 24. Aun así, try suele preferirse para archivos, porque el archivo podría desaparecer entre la revisión y la apertura. Discute con tu grupo: ¿esto contradice la regla del curso o la complementa?
🟠 Reto: clasifica estas situaciones como "if/else" o "excepción" y justifica: (a) el usuario deja un campo vacío; (b) el usuario escribe letras donde va un número; (c) un archivo CSV tiene una fila con menos columnas; (d) la nota está fuera del rango de 1.0 a 5.0; (e) no hay conexión a internet.
09
7. 🌟 Proyecto extra: el inventario del aula de tecnología#
El docente de tecnología necesita llevar el inventario del aula (portátiles, kits de robótica, cables, sensores) y saber quién tiene prestado cada equipo.
Datos en inventario.json, con esta estructura (puedes mejorarla):
JSON
1234
{"PORT-01":{"tipo":"Portátil","estado":"Bueno","prestado_a":null},"ARD-05":{"tipo":"Kit Arduino","estado":"Falta un sensor","prestado_a":"Grupo 3"}}
Interfaz Tkinter con campos para el código, el tipo y el estado, y botones Agregar/Actualizar, Buscar, Prestar y Devolver.
Buscar muestra toda la información de un código en una ventana emergente.
Prestar falla con un mensaje claro si el equipo ya está prestado o si no existe.
Un botón Resumen que muestre cuántos equipos hay, cuántos están prestados y la lista de códigos disponibles, construida con una comprensión.
Robustez: el programa no se cae si inventario.json no existe ni si está vacío o dañado. En ese último caso, avisa al usuario y no sobrescribe el archivo sin preguntar.
Se crea inventario.json al agregar el primer equipo
Prestar un equipo disponible
prestado_a se actualiza
Prestar un equipo ya prestado
Mensaje de error; no cambia nada
Buscar un código inexistente
Mensaje claro
inventario.json con el texto {roto
Aviso de archivo dañado; el programa no se cae
Un tipo con tilde: "Cámara"
En el JSON se ve "Cámara", no á
10
8. 🏆 Proyecto integrador de los Días 26 a 30: el Gestor de la Huerta Escolar#
Proyecto integrador creado para esta adaptación. Integra comprensiones de listas y diccionarios, pandas e iterrows (Día 26); Tkinter, argumentos por defecto, *args y **kwargs (Día 27); Canvas, after y tipado dinámico (Día 28); validación, ventanas emergentes y columnspan (Día 29); y JSON y excepciones (Día 30).
El proyecto ambiental del colegio tiene una huerta escolar. Cada grupo cuida algunas plantas, pero se olvidan de regarlas, nadie sabe cuándo se sembró cada una y la información está en una hoja de cálculo desactualizada.
Construir una aplicación de escritorio con Tkinter que registre las plantas de la huerta, recuerde cuándo regarlas y guarde toda la información en JSON.
Importación inicial (Día 26): leer plantas_iniciales.csv (con las columnas planta, cama, grupo, riego_cada_dias, fecha_siembra) con pandas y convertirlo en un diccionario con una comprensión e iterrows (o la alternativa con zip, justificando tu elección).
Almacenamiento (Día 30): todo se guarda en huerta.json. Si no existe, se crea desde el CSV; si está dañado, se avisa sin perder datos.
Interfaz (Días 27 y 29): diseño con grid y al menos un columnspan; campos para registrar o actualizar una planta; Radiobutton o menú para la cama; Spinbox para los días entre riegos.
Validación (Días 29 y 30): una función validar_planta(**datos) que lance ValueError con un mensaje claro si falta un dato o si un valor es imposible (días de riego ≤ 0, fecha futura…). La interfaz lo captura y lo muestra con messagebox.
Registro de riego: un botón "Regué hoy" que actualice la fecha del último riego de la planta seleccionada.
Panel de alertas (Día 26): con una comprensión, la lista de plantas que necesitan riego hoy, mostrada en una etiqueta o Listbox, con colores según la urgencia.
Temporizador de riego (Día 28): un temporizador con after para las jornadas de riego (por ejemplo, 2 minutos por cama), que avance solo a la siguiente cama y muestre MM:SS sobre un dibujo en un Canvas.
Funciones flexibles (Día 27): al menos una función con valores por defecto y una con *args (por ejemplo, regar(*camas)).
Crea cargar_json(ruta, por_defecto=None) que devuelva el contenido de un archivo JSON, o por_defecto si el archivo no existe o está vacío. Úsala en MyPass para eliminar código repetido.
Actividad 1: los mensajes de error como pista. El curso insiste en leer los errores. Antes de pegar un error en una IA:
Copia el tipo de excepción y la última línea del traceback.
Escribe tu hipótesis: ¿qué crees que pasó?
Solo entonces pregúntale a la IA, y compara su explicación con tu hipótesis.
Actividad 2: verificar una afirmación técnica. Pregúntale a una IA: "En Python, ¿json.load recibe la ruta del archivo o el archivo abierto?" y "¿Es update un método del módulo json?". Luego:
Comprueba cada respuesta ejecutando código (sección 4, errores frecuentes).
Observa que la transcripción del curso tiene estas mismas imprecisiones (sección 11): ni un video ni una IA son infalibles. La fuente final es la documentación y la ejecución.
Actividad 3: ¿la IA abusa de las excepciones? Pídele que escriba la búsqueda de MyPass. Si usa try/except KeyError para el sitio que no existe, pregúntale por qué no usó if/else, y discute su respuesta con la regla del curso (sección 6).
13
11. 🛠️ Correcciones técnicas al material original#
Archivo
Afirmación o comportamiento del curso
Observación
Tratamiento
228
"Ley de Morphy"
Es la ley de Murphy; error de transcripción
Tema 1
228
Ejemplo con open sin with y file.close() en finally
Funciona porque file se asigna en todos los caminos; si fallara antes de abrir, close() daría NameError
Nota técnica del Tema 1
228
except KeyError as error_message imprime la clave
El mensaje incluye comillas: The key 'sdfsdf' does not exist. (verificado)
Tema 1
230
La función se llama a sí misma para volver a preguntar
Válido; tras ~1000 errores seguidos daría RecursionError (límite verificado: 1000). Alternativa con while True
Nota técnica del Tema 3
231
"JSON dot update"
update es un método de los diccionarios: data.update(new_data)
Nota técnica del Tema 4
231
A json.load "le pasamos la ruta del fichero"
Recibe el objeto archivo abierto, no la ruta
Nota técnica del Tema 4
231, 232, 233
Solo se captura FileNotFoundError
Un data.json vacío produce JSONDecodeError sin capturar (verificado)
🧪 del proyecto
233
La búsqueda usa if website in data
Distingue mayúsculas: "amazon" no encuentra "Amazon" (verificado)
🧪 del proyecto
233
"No tengo una manera fácil de verificar si este archivo de datos no se encuentra"
Existe Path(...).exists() (Día 24), aunque try sigue siendo preferible para archivos
El ejemplo de a_file.txt con los cuatro bloques y tu explicación de cuándo se ejecuta cada uno.
La calculadora del IMC con las validaciones del Reto 1 del Tema 2.
El deletreador OTAN con manejo de excepciones (en la versión recursiva y en la versión con while).
MyPass 2.0 funcionando con datos ficticios, con la tabla de pruebas y los problemas 1 y 2 de 🧪 corregidos.
El inventario del aula (proyecto extra) o el Gestor de la Huerta Escolar (proyecto integrador).
Cuaderno de errores:FileNotFoundError, KeyError con su mensaje, except desnudo que esconde errores, RecursionError, AttributeError por pasar la ruta a json.load, JSONDecodeError con un archivo vacío, pérdida de datos por abrir en "w" antes de leer, y búsquedas que fallan por mayúsculas.