Saltar al contenido
Inicio 100 Días de Código en Python — Día 30: Errores, excepciones y datos JSON

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

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.


Tabla de contenidos

  1. Antes de empezar
  2. Capturar excepciones: try, except, else y finally
  3. Lanzar tus propias excepciones con raise
  4. Ejercicio: excepciones en el proyecto de la OTAN
  5. JSON: escribir, leer y actualizar datos
  6. 🚀 Proyecto del día: MyPass 2.0 con JSON, excepciones y búsqueda
  7. ¿Excepción o if/else? La regla del curso
  8. 🌟 Proyecto extra: el inventario del aula de tecnología
  9. 🏆 Proyecto integrador de los Días 26 a 30: el Gestor de la Huerta Escolar
  10. 🧩 Comprueba lo aprendido
  11. 🤖 Programar con apoyo de IA
  12. 🛠️ Correcciones técnicas al material original
  13. 📋 Matriz de trazabilidad
  14. 📦 Evidencias de aprendizaje del Día 30

0. Antes de empezar

(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:

  1. Errores y excepciones: cómo evitar que un programa se "caiga" cuando algo sale mal.
  2. 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).


1. Capturar excepciones: try, except, else y finally

(Fuente: 228 - Catching Exceptions- The try catch except finally Pattern.md)

🎯 Objetivo de aprendizaje

  • Reconocer los errores más comunes de Python.
  • Capturar excepciones específicas y reaccionar ante ellas.
  • Explicar cuándo se ejecuta cada bloque.

🧠 ¿Qué aprenderemos?

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.

📖 Explicación conceptual

Los errores que ya conocemos (repaso del curso):

Error Cuándo ocurre Ejemplo
FileNotFoundError Abrir para leer un archivo que no existe open("a_file.txt")
KeyError Pedir una clave que no está en el diccionario {"key": "value"}["no_existe"]
IndexError Pedir una posición que no existe en una lista ["a", "b", "c"][3]
TypeError Operar con tipos incompatibles "abc" + 5

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
try:
    # Algo que PODRÍA fallar
except:
    # Qué hacer SI hubo una excepción
else:
    # Qué hacer si NO hubo excepción
finally:
    # 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:

Python
try:
    file = open("a_file.txt")
except:
    file = open("a_file.txt", "w")
    file.write("Something")

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

⚠️ Errores frecuentes

Error Qué ocurre Solución
except: sin tipo Esconde errores que no esperabas Captura excepciones específicas
Poner demasiado código en el try No sabes qué línea falló Deja en el try solo lo riesgoso; lo demás va en el else
Escribir mal el nombre: except FileNotFound: NameError cuando ocurre la excepción Copia el nombre exacto del mensaje de error
Creer que else se ejecuta "si no entró al except" aunque haya un return o un error en el else Confusión al leer el código else = el try terminó sin excepciones

Retos adicionales (creados para esta adaptación)

  • 🟢 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 separado ValueError (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.

2. Lanzar tus propias excepciones con raise

(Fuente: 229 - Raising your own Exceptions.md)

🎯 Objetivo de aprendizaje

  • Lanzar una excepción con raise cuando los datos son válidos para Python, pero no para el problema.

📖 Explicación conceptual

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
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
height = float(input("Height: "))   # en metros
weight = int(input("Weight: "))     # en kilogramos

if height > 3:
    raise ValueError("Human Height should not be over 3 meters.")

bmi = weight / height ** 2
print(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.

Retos adicionales (creados para esta adaptación)

  • 🟢 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.

3. Ejercicio: excepciones en el proyecto de la OTAN

(Fuente: 230 - Code Exercise- Exception Handling in the NATO Phonetic Alphabet Project.md)

📝 Ejercicio original del curso

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.

🔍 Antes de programar

  • ¿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
def generate_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
while True:
    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.

Retos adicionales (creados para esta adaptación)

  • 🟢 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.

4. JSON: escribir, leer y actualizar datos

(Fuente: 231 - Write, read and update JSON data in the Password Manager.md)

🎯 Objetivo de aprendizaje

  • Explicar qué es JSON y por qué se parece a un diccionario de Python.
  • Escribir, leer y actualizar un archivo JSON con el módulo json.

📖 Explicación conceptual

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:

JSON
{
    "Amazon": {
        "email": "estudiante@ejemplo.com",
        "password": "Clave-De-Prueba-123"
    },
    "eBay": {
        "email": "estudiante@ejemplo.com",
        "password": "Otra-Clave-456"
    }
}

La clave de primer nivel es el sitio web, porque es lo que vamos a buscar.

Las tres operaciones con el módulo json (viene incluido con Python):

Operación Función Modo del archivo Qué hace
Escribir json.dump(datos, archivo, indent=4) "w" Convierte un diccionario de Python en JSON y lo guarda
Leer json.load(archivo) "r" Lee el JSON y lo convierte en un diccionario de Python
Actualizar load → datos.update(nuevos) → dump "r" y luego "w" Lee, modifica y vuelve a guardar
  • indent=4 hace el archivo legible para humanos; sin él, todo queda en una sola línea.
  • json.load devuelve un dict normal: el curso lo confirma con type().
  • Convertir a JSON para guardar y volver a Python para trabajar se llama serializar y deserializar.

Actualizar: el enfoque de tres pasos del curso.

  1. Leer los datos antiguos.
  2. Actualizar los datos antiguos con los nuevos.
  3. Guardar los datos actualizados.
Python
new_data = {
    website: {
        "email": email,
        "password": password,
    }
}

with open("data.json", "r") as data_file:
    data = json.load(data_file)        # 1. leer
    data.update(new_data)              # 2. actualizar

with open("data.json", "w") as data_file:
    json.dump(data, data_file, indent=4)   # 3. guardar

¿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, update reemplaza 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.

⚠️ Errores frecuentes

Error Qué ocurre Solución
json.load("data.json") con el nombre como texto AttributeError: 'str' object has no attribute 'read' Pasa el archivo abierto: json.load(data_file)
Abrir en "w" antes de leer El modo "w" borra el archivo al abrirlo: pierdes todos los datos Primero lee ("r"), después escribe ("w")
Agregar con "a" JSON inválido Leer → actualizar → escribir
Archivo JSON vacío (0 bytes) json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) (verificado) Ver 🧪 del proyecto

Retos adicionales (creados para esta adaptación)

  • 🟢 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?

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)

Problema

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.

Requisitos funcionales

  1. Guardar en data.json con la estructura anidada de la sección 4.
  2. La primera vez (sin data.json), guardar sin errores.
  3. Si ya hay datos, actualizarlos sin perder los anteriores.
  4. Siempre, al final, borrar el sitio web y la contraseña de las entradas.
  5. Un botón Search junto al sitio web que muestre el correo y la contraseña en una ventana emergente.
  6. Mensajes claros si no hay archivo o si el sitio web no está guardado.

Construcción paso a paso

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.

🔍 Antes de programar

  • ¿Qué línea es "peligrosa"?
  • Si falla, ¿qué debes escribir en el archivo nuevo: data o new_data? ¿Por qué?
  • ¿Qué bloque se ejecuta solo si la lectura funcionó?
  • ¿Qué debe pasar siempre, haya error o no?

Solución del curso (estructura):

Python
try:
    with open("data.json", "r") as data_file:
        data = json.load(data_file)
except FileNotFoundError:
    with open("data.json", "w") as data_file:
        json.dump(new_data, data_file, indent=4)
else:
    data.update(new_data)
    with open("data.json", "w") as data_file:
        json.dump(data, data_file, indent=4)
finally:
    website_entry.delete(0, END)
    password_entry.delete(0, END)

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):

  1. Obtener el sitio web de la entrada.
  2. Abrir data.json y cargar los datos.
  3. 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.

  1. Sin data.json (primera vez): FileNotFoundError → ventana "Error" — "No Data File Found.".
  2. 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.".

Pruebas

Caso Esperado
Sin data.json → Add Se crea data.json con un sitio
Con datos → Add de un sitio nuevo El sitio se agrega; los anteriores siguen ahí
Add de un sitio que ya existe Sus datos se reemplazan (no se duplican)
Sin data.json → Search "No Data File Found."
Search de un sitio guardado Ventana con el correo y la contraseña
Search de un sitio no guardado "No details for … exists."
Search de amazon cuando se guardó Amazon ⚠️ (ver 🧪)
data.json vacío (0 bytes) → Add o Search ⚠️ (ver 🧪)

🧪 Prueba y depuración

  1. 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.
  2. 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?
  3. 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?
  4. 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.

⭐ Extensión del proyecto

  • Refactorizar: una función guardar_datos(datos) para eliminar la escritura repetida.
  • Volver a poner la confirmación askokcancel antes de guardar.
  • Un botón Copy en la ventana de búsqueda que copie la contraseña al portapapeles.
  • Un botón Delete que elimine un sitio guardado (investiga del o pop en diccionarios), con confirmación.

6. ¿Excepción o if/else? La regla del curso

(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 adicional (creado para esta adaptación)

  • 🟠 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.

7. 🌟 Proyecto extra: el inventario del aula de tecnología

Situación

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.

Requisitos

  1. Datos en inventario.json, con esta estructura (puedes mejorarla):
JSON
   {
       "PORT-01": {"tipo": "Portátil", "estado": "Bueno", "prestado_a": null},
       "ARD-05": {"tipo": "Kit Arduino", "estado": "Falta un sensor", "prestado_a": "Grupo 3"}
   }
  1. Interfaz Tkinter con campos para el código, el tipo y el estado, y botones Agregar/Actualizar, Buscar, Prestar y Devolver.
  2. Buscar muestra toda la información de un código en una ventana emergente.
  3. Prestar falla con un mensaje claro si el equipo ya está prestado o si no existe.
  4. 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.
  5. 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.

Restricciones técnicas

  • Al menos un raise ValueError en una función de validación (por ejemplo, un código con formato inválido) que la interfaz capture con try/except.
  • Excepciones solo donde se justifiquen según la regla de la sección 6. Explica en un comentario por qué usaste if/else o try en cada caso.
  • json.dump con indent=4 y ensure_ascii=False (investiga para qué sirve este último con las tildes y la ñ).

Casos de prueba

Caso Esperado
Primera ejecución 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 á

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

Situación problema

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.

Pregunta orientadora

¿Cómo puede una aplicación sencilla ayudar a una comunidad a cuidar algo en común sin depender de la memoria de una sola persona?

Objetivo

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.

Requerimientos

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. Registro de riego: un botón "Regué hoy" que actualice la fecha del último riego de la planta seleccionada.
  6. 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.
  7. 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.
  8. Funciones flexibles (Día 27): al menos una función con valores por defecto y una con *args (por ejemplo, regar(*camas)).

Restricciones

  • Ningún error del usuario debe aparecer en la consola: todos se muestran en la interfaz.
  • Ningún except desnudo.
  • Nada de time.sleep en la interfaz.
  • No guardar datos personales de los estudiantes: los grupos se identifican como "Grupo 1", "Grupo 2"…

Etapas de desarrollo

  1. Análisis: entrevista a quien coordina la huerta (o a tu profesor) y define qué datos necesitan realmente.
  2. Diseño: dibuja la cuadrícula de la interfaz y la estructura de huerta.json.
  3. Datos: importación del CSV y lectura/escritura del JSON con manejo de excepciones.
  4. Interfaz: formulario, validación y ventanas emergentes.
  5. Alertas y temporizador.
  6. Pruebas y mejora: ejecuta los casos de prueba, corrige y documenta.

Entregables

  • Código organizado por secciones (como el main.py del curso), con docstrings en las funciones.
  • plantas_iniciales.csv y un huerta.json de ejemplo.
  • Un registro de errores y soluciones del proceso.
  • Una demostración de 5 minutos para el proyecto ambiental.
  • Una reflexión: ¿qué haría falta para que varios computadores compartieran los mismos datos? (Pista: lo que viene en el curso sobre APIs e internet.)

Casos de prueba

Caso Esperado
Primera ejecución sin huerta.json Se crea a partir del CSV
huerta.json dañado Aviso claro; no se sobrescribe sin confirmación
Registrar una planta con riego cada 0 días ValueError capturado y mostrado
Planta regada hace más días de los indicados Aparece en las alertas
Pulsar "Regué hoy" Desaparece de las alertas y se guarda en el JSON
Iniciar el temporizador dos veces No se crean dos cuentas simultáneas
Buscar una planta escrita con otras mayúsculas La encuentra

Criterios técnicos de funcionamiento

  • El programa arranca y funciona sin huerta.json y con un huerta.json dañado.
  • Todas las comprensiones son legibles; si una no lo es, se reemplaza por un bucle con un comentario que explique por qué.
  • Cada excepción capturada tiene una justificación según la regla de la sección 6.
  • Los datos sobreviven a cerrar y abrir la aplicación.

Posibles extensiones

  • Exportar un informe semanal a CSV con pandas.
  • Una gráfica sencilla en el Canvas con los riegos por grupo.
  • Registrar la cosecha y calcular cuántos kilos produjo cada cama.

9. 🧩 Comprueba lo aprendido

Conceptuales

  1. Explica cuándo se ejecuta cada bloque: try, except, else y finally.
  2. ¿Por qué PEP 8 desaconseja el except desnudo? Da un ejemplo en el que esconda un error.
  3. ¿Qué diferencia hay entre json.dump y json.load? ¿Qué tipo de dato devuelve json.load?

Análisis de código

Python
try:
    edad = int(input("Edad: "))
except ValueError:
    print("A")
else:
    print("B")
finally:
    print("C")

¿Qué imprime si el usuario escribe 16? ¿Y si escribe dieciséis?

Predicción

Python
datos = {"Amazon": {"email": "a@x.com"}}
nuevos = {"Amazon": {"email": "b@x.com"}, "eBay": {"email": "c@x.com"}}
datos.update(nuevos)
print(len(datos), datos["Amazon"]["email"])

Depuración

Encuentra cuatro problemas en esta función de búsqueda.

Python
def find_password():
    website = website_entry.get()
    try:
        data = json.load("data.json")
    except:
        messagebox.showinfo(title="Error", message="No Data File Found.")
    if website in data:
        messagebox.showinfo(title=website, message=data["website"]["password"])

Modificación

Modifica MyPass 2.0 para que Search muestre también cuántos días hace que se guardó esa contraseña. (Tendrás que guardar la fecha al registrar.)

Creación

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.


10. 🤖 Programar con apoyo de IA

Actividad 1: los mensajes de error como pista. El curso insiste en leer los errores. Antes de pegar un error en una IA:

  1. Copia el tipo de excepción y la última línea del traceback.
  2. Escribe tu hipótesis: ¿qué crees que pasó?
  3. 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:

  1. Comprueba cada respuesta ejecutando código (sección 4, errores frecuentes).
  2. Compárala con la documentación oficial del módulo json (se abre en otra pestaña).
  3. 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).


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 Sección 6

12. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Objetivos: excepciones, JSON, búsqueda 227 Sección 0 — — Sección 5
Errores comunes, try/except/else/finally, PEP 8, as 228 Tema 1 Ejemplo de a_file.txt Retos 1–4 del Tema 1 Secciones 5, 7 y 8
raise, ValueError, IMC 229 Tema 2 Estatura mayor de 3 m Retos 1–4 del Tema 2 Secciones 7 y 8
Excepciones en el proyecto OTAN 230 Tema 3 generate_phonetic() Retos 1–4 del Tema 3 Día 26
JSON, dump, load, indent, update, leer-actualizar-escribir 231 Tema 4 MyPass con JSON Retos 1–4 del Tema 4 Secciones 5, 7 y 8
FileNotFoundError al guardar 232 Sección 5, versión 2 Desafío 1 Depuración (archivo vacío, finally) Secciones 7 y 8
Botón Search, find_password, diccionarios anidados, if/else frente a excepciones 233 Sección 5, versiones 3 a final; sección 6 Desafío 2 Depuración (mayúsculas); reto de la sección 6 Secciones 7 y 8
(No existe en el curso original) — — — Inventario del aula de tecnología Sección 7
(No existe en el curso original) — — — Gestor de la Huerta Escolar (Días 26–30) Sección 8

13. 📦 Evidencias de aprendizaje del Día 30

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