Saltar al contenido
Inicio 100 Días de Código en Python — Día 34: Práctica con APIs — Quizzler, la aplicación de preguntas con interfaz gráfica

100 Días de Código en Python — Día 34: Práctica con APIs — Quizzler, la aplicación de preguntas con interfaz gráfica

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 34 (nivel intermedio+).

Este tutorial adapta pedagógicamente el Día 34. 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: la API de Open Trivia DB se consultó en vivo el 26 de septiembre de 2026 con Python 3.14 y requests 2.34. La lógica del cuestionario se verificó con una réplica de las clases del Día 17. El proyecto inicial (imágenes true.png y false.png, y los archivos del Día 17) no está en la carpeta; se puede reconstruir con el proyecto del Día 17 y dos imágenes cualesquiera.


Tabla de contenidos

  1. Antes de empezar: del cuestionario de consola a Quizzler
  2. Preguntas nuevas en cada ejecución con Open Trivia DB
  3. Entidades HTML y html.unescape
  4. Una interfaz Tkinter dentro de una clase
  5. Mostrar la siguiente pregunta en la interfaz
  6. Anotaciones de tipo (type hints) y la flecha ->
  7. Verificar la respuesta, dar retroalimentación y llevar el puntaje
  8. 🚀 Proyecto del día: Quizzler completo
  9. 🌟 Proyecto extra: el Concurso del Día de la Ciencia
  10. 🏆 Proyecto integrador de los Días 33 y 34: el Tablero de Datos en Vivo
  11. 🧩 Comprueba lo aprendido
  12. 🤖 Programar con apoyo de IA
  13. 🛠️ Correcciones técnicas al material original
  14. 📋 Matriz de trazabilidad
  15. 📦 Evidencias de aprendizaje del Día 34

0. Antes de empezar: del cuestionario de consola a Quizzler

(Fuentes: 259 - Day 34 Goals- what you will make by the end of the day.md, 260 - Trivia Question API Challenge.md)

Hoy es un día de repaso aplicado. Vamos a mejorar el cuestionario de verdadero o falso del Día 17:

Día 17 (antes) Día 34 (hoy)
Preguntas escritas a mano en data.py Preguntas nuevas en cada ejecución, traídas de una API
Respuestas escritas en la consola Botones ✔ y ✘ en una ventana
Puntaje impreso con print Puntaje en pantalla y tarjeta que se pone verde o roja

La aplicación se llamará Quizzler. Las preguntas vienen de Open Trivia DB, una base de datos abierta con miles de preguntas verificadas.

Recordatorio de la estructura del Día 17:

Archivo Responsabilidad
data.py Los datos de las preguntas (question_data)
question_model.py Clase Question: texto y respuesta
quiz_brain.py Clase QuizBrain: pregunta actual, número, puntaje, verificar respuesta
main.py Crea el banco de preguntas y el cuestionario
ui.py (nuevo) Clase QuizInterface: la ventana

Prerequisito general: clases, __init__, atributos y métodos (Días 16 y 17); el proyecto del cuestionario (Día 17); Tkinter, Canvas, grid, after (Días 27 a 31); APIs, parámetros y raise_for_status (Día 33).


1. Preguntas nuevas en cada ejecución con Open Trivia DB

(Fuentes: 260 - Trivia Question API Challenge.md, 261 - Solution & Walkthrough for getting Trivia Questions.md)

🎯 Objetivo de aprendizaje

Reemplazar datos escritos a mano por datos obtenidos de una API, sin cambiar el resto del programa.

📝 Ejercicio original del curso

Enunciado: modificar data.py para que obtenga 10 preguntas nuevas de Open Trivia DB cada vez que se ejecuta el programa. La categoría y la dificultad se dejan libres; el tipo debe ser verdadero/falso.

En la página de Open Trivia DB se usa el generador de URL de la API, que produce algo así:

Texto
https://opentdb.com/api.php?amount=10&type=boolean

Recuerda (Día 33): el endpoint es todo lo que va antes del ?. Lo demás son parámetros: amount=10 y type=boolean.

🔍 Antes de programar

  • ¿Qué nombre debe tener la variable con las preguntas para que main.py siga funcionando sin cambios?
  • La respuesta de la API es un diccionario con dos claves. ¿Cuál contiene la lista de preguntas?

🔎 La solución del curso

Python
import requests

parameters = {
    "amount": 10,
    "type": "boolean",
}

response = requests.get("https://opentdb.com/api.php", params=parameters)
response.raise_for_status()
data = response.json()
question_data = data["results"]

La respuesta real tiene esta forma (verificada):

JSON
{
  "response_code": 0,
  "results": [
    {"type": "boolean", "difficulty": "medium", "category": "General Knowledge",
     "question": "The French word for "glass" is "glace".",
     "correct_answer": "False", "incorrect_answers": ["True"]},
    ...
  ]
}

Cada elemento de results tiene las mismas claves que usaba el question_data escrito a mano en el Día 17 (question, correct_answer). Por eso main.py funciona sin tocarlo: es una ventaja de haber separado las responsabilidades en archivos.

Truco del curso: para probar solo data.py, haz clic derecho sobre el archivo y elige Run 'data'.

📖 El response_code de Open Trivia DB

Además del código HTTP, esta API incluye su propio código en el JSON. 0 significa éxito. En la prueba de esta adaptación, ejecutar el programa dos veces seguidas produjo (verificado):

Texto
HTTP 429  {"response_code":5,"result":[]}

El código HTTP 429 significa Too Many Requests (demasiadas peticiones): la API limita cuántas peticiones puedes hacer en pocos segundos. Aquí raise_for_status() sí detiene el programa, pero con un error que un estudiante no entiende fácilmente.

🏫 Para el aula: si 30 estudiantes comparten la misma conexión a internet, para la API todos vienen de la misma dirección. Es muy probable que reciban el error 429. Solución práctica: que cada estudiante guarde en un preguntas.json la primera respuesta exitosa y la use mientras desarrolla la interfaz (sección 7, 🧪).

⚠️ Errores frecuentes

Error Qué ocurre Solución
Llamar la variable questions main.py falla con ImportError Debe llamarse question_data
question_data = data main.py recibe el diccionario completo, no la lista data["results"]
"type": "true/false" La API no reconoce el valor Usa exactamente "boolean"
Ejecutar muchas veces seguidas HTTP 429 Espera unos segundos o guarda las preguntas en un archivo

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: cambia amount a 5 y ejecuta el cuestionario de consola.
  • 🟡 Reto 2: agrega el parámetro category con el número 18 (Science: Computers, verificado en https://opentdb.com/api_category.php) y luego con otra categoría que te interese.
  • 🟠 Reto 3: revisa data["response_code"] y, si no es 0, muestra un mensaje claro según su valor (consulta la documentación de la API).
  • 🔴 Reto 4: haz que, si la API falla por cualquier motivo, el programa use las preguntas escritas a mano del Día 17 como plan B.

2. Entidades HTML y html.unescape

(Fuente: 262 - Unescaping HTML Entities.md)

🎯 Objetivo de aprendizaje

Reconocer las entidades HTML y convertirlas en texto legible.

📖 Explicación conceptual

Las preguntas llegan con símbolos extraños: &quot;glace&quot; o Stalin&#039;s. Son entidades HTML: una forma de escribir ciertos caracteres para que no se confundan con el código HTML. Por ejemplo, < podría ser el inicio de una etiqueta, así que se escribe &lt;.

Entidad Carácter
&quot; "
&#039; '
&amp; &
&lt; / &gt; < / >

El camino del curso: probar primero en una herramienta en línea (unescape), y luego buscar en Google "unescape HTML entities Python". El primer resultado de Stack Overflow da la respuesta: el módulo html, que viene con Python.

Python
import html

print(html.unescape("The Cold War ended with Joseph Stalin&#039;s death."))
# The Cold War ended with Joseph Stalin's death.

(Verificado.)

Dónde lo pone el curso: en QuizBrain.next_question, al preparar el texto de la pregunta:

Python
q_text = html.unescape(self.current_question.text)

Mensaje del curso: "Como programador, encontrar soluciones a tus propios problemas es una habilidad que debes perfeccionar". Saber qué buscar (aquí: "unescape") es la mitad del trabajo.

💡 Alternativa válida (adaptación): hacer el unescape en data.py, una sola vez al recibir los datos, en lugar de cada vez que se muestra una pregunta. Así QuizBrain recibe texto ya limpio y no necesita saber que los datos vinieron de una página web. Las dos opciones funcionan; compáralas en el Reto 3.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: aplica html.unescape a "Tom &amp; Jerry &lt;3" y predice el resultado antes de ejecutarlo.
  • 🟡 Reto 2: el proceso inverso existe: investiga html.escape y úsalo con un texto que tenga < y &.
  • 🟠 Reto 3: mueve el unescape a data.py, con una comprensión de listas que limpie todas las preguntas. ¿Qué ventaja tiene frente a la solución del curso?
  • 🔴 Reto 4: ¿las respuestas (correct_answer) también podrían traer entidades? Busca en la API una pregunta de selección múltiple con entidades en sus opciones y asegúrate de limpiarlas también.

3. Una interfaz Tkinter dentro de una clase

(Fuente: 263 - Class based Tkinter UI.md)

🎯 Objetivo de aprendizaje

Construir una interfaz gráfica como una clase, con los elementos como atributos.

📖 Explicación conceptual

Hasta ahora, las interfaces Tkinter se escribían con variables sueltas y funciones que usaban global (Día 31). Siguiendo la idea del Día 17 de separar responsabilidades, ahora la interfaz es una clase: QuizInterface.

  • Los nombres de clase se escriben en PascalCase: QuizInterface.
  • Todo lo que otros métodos necesitarán se guarda con self.: self.window, self.canvas, self.score_label.
  • self.window.mainloop() va al final de __init__.
  • En main.py, el antiguo while del cuestionario de consola se comenta: mainloop ya es un bucle infinito que atiende la ventana, y otro while lo bloquearía.

📝 Ejercicio original del curso

Enunciado: construir la interfaz de la diapositiva del curso dentro de __init__:

  • Ventana con margen de 20 píxeles y fondo del color del tema.
  • Etiqueta "Score: 0" en blanco, arriba a la derecha.
  • Lienzo blanco de 300 × 250 con el texto de la pregunta en Arial 20 cursiva, separado 50 píxeles arriba y abajo.
  • Botones con las imágenes true.png y false.png.

🔎 La solución del curso (reconstruida)

Python
from tkinter import *

THEME_COLOR = "#375362"   # usa el valor que trae ui.py del proyecto inicial

class QuizInterface:

    def __init__(self):
        self.window = Tk()
        self.window.title("Quizzler")
        self.window.config(padx=20, pady=20, bg=THEME_COLOR)

        self.score_label = Label(text="Score: 0", fg="white", bg=THEME_COLOR)
        self.score_label.grid(row=0, column=1)

        self.canvas = Canvas(width=300, height=250, bg="white")
        self.question_text = self.canvas.create_text(
            150, 125,
            text="Some Question Text",
            fill=THEME_COLOR,
            font=("Arial", 20, "italic")
        )
        self.canvas.grid(row=1, column=0, columnspan=2, pady=50)

        true_image = PhotoImage(file="images/true.png")
        self.true_button = Button(image=true_image, highlightthickness=0)
        self.true_button.grid(row=2, column=0)

        false_image = PhotoImage(file="images/false.png")
        self.false_button = Button(image=false_image, highlightthickness=0)
        self.false_button.grid(row=2, column=1)

        self.window.mainloop()

Y en main.py:

Python
from ui import QuizInterface
# ...
quiz_ui = QuizInterface()

Detalles de la solución:

  • La etiqueta está en la columna 1, pero al principio aparece a la izquierda: sin nada en la columna 0, la columna 1 queda pegada al borde. Cuando el lienzo ocupa dos columnas (columnspan=2), la etiqueta pasa a la derecha.
  • La etiqueta tenía fondo blanco y el texto blanco no se veía: por eso lleva bg=THEME_COLOR.
  • El error del curso: create_text(text=...) sin coordenadas produjo IndexError: tuple index out of range, un mensaje confuso. Todo lo que se agrega a un lienzo necesita primero su posición X e Y: (150, 125) es el centro de un lienzo de 300 × 250.
  • true_image y false_image no llevan self. porque solo se usan dentro de __init__.

🛠️ Nota técnica (análisis de esta adaptación): que true_image sea una variable local funciona solo porque mainloop() está dentro de __init__: el método no termina mientras la ventana está abierta, y la imagen sigue existiendo. Si movieras mainloop() fuera de __init__ (por ejemplo, a main.py), las imágenes se borrarían al terminar __init__ y los botones quedarían en blanco: es el problema de recolección de basura del Día 31. Guardarlas como self.true_image evita ese riesgo.

⚠️ Errores frecuentes

Error Qué ocurre Solución
Olvidar self. en window Otros métodos no pueden usar la ventana self.window = Tk()
Dejar el while del Día 17 en main.py La ventana no aparece o se congela Coméntalo
create_text sin coordenadas IndexError: tuple index out of range create_text(150, 125, ...)
Código después de mainloop() dentro de __init__ No se ejecuta hasta cerrar la ventana Todo va antes de mainloop()

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: agrega una etiqueta con el nombre de la categoría de la pregunta, arriba a la izquierda.
  • 🟡 Reto 2: mueve mainloop() a main.py (quiz_ui.window.mainloop()). ¿Qué pasa con las imágenes? Corrígelo con self..
  • 🟠 Reto 3: si no tienes las imágenes, crea los botones con text="✔" y text="✘", fuente grande y colores. Compara las dos versiones.
  • 🔴 Reto 4: organiza __init__ en métodos privados: _crear_marcador(), _crear_tarjeta() y _crear_botones(). ¿Por qué es más fácil de leer?

4. Mostrar la siguiente pregunta en la interfaz

(Fuente: 264 - Python Typing & Showing the Next Question in the GUI.md)

🎯 Objetivo de aprendizaje

Conectar dos objetos: la interfaz pide datos al "cerebro" del cuestionario y los muestra.

📖 Explicación conceptual

Problema: QuizBrain.next_question() usaba input() para preguntar en la consola. En la versión gráfica, la respuesta llega por los botones. Por eso el curso cambia el método para que devuelva el texto de la pregunta:

Python
def next_question(self):
    self.current_question = self.question_list[self.question_number]
    self.question_number += 1
    q_text = html.unescape(self.current_question.text)
    return f"Q.{self.question_number}: {q_text}"
    # user_answer = input(...)       ← se comenta
    # self.check_answer(user_answer) ← se comenta

¿Cómo obtiene la interfaz el QuizBrain? Se lo pasamos al crearla:

Python
# main.py
quiz = QuizBrain(question_bank)
quiz_ui = QuizInterface(quiz)
Python
# ui.py
from quiz_brain import QuizBrain

class QuizInterface:

    def __init__(self, quiz_brain: QuizBrain):
        self.quiz = quiz_brain
        # ... interfaz ...
        self.get_next_question()
        self.window.mainloop()

    def get_next_question(self):
        q_text = self.quiz.next_question()
        self.canvas.itemconfig(self.question_text, text=q_text)
  • quiz_brain: QuizBrain es una anotación de tipo (sección 5): le dice al editor qué clase de objeto llega. Gracias a eso, al escribir self.quiz. el editor sugiere los métodos de QuizBrain. Sin ella, no sugería nada.
  • get_next_question() se llama en __init__ antes de mainloop(), para que la primera pregunta aparezca al abrir.
  • Texto que no cabe: el parámetro width=280 en create_text hace que el texto pase a varias líneas dentro de los 300 píxeles del lienzo.

Principio del curso: cada clase se ocupa de lo suyo. QuizBrain administra el cuestionario (preguntas, puntaje); QuizInterface solo muestra y recibe clics.

⚠️ Errores frecuentes

Error Qué ocurre Solución
Crear un QuizBrain nuevo dentro de ui.py Hay dos cuestionarios distintos Recíbelo como parámetro
No comentar el input() de next_question La ventana se congela esperando la consola Comenta las líneas de consola
No llamar get_next_question() en __init__ Aparece "Some Question Text" Llámalo antes de mainloop()

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: muestra el número de la pregunta con el total: "Pregunta 3 de 10".
  • 🟡 Reto 2: muestra la dificultad de la pregunta en una etiqueta. ¿Qué tienes que cambiar en Question y en data.py?
  • 🟠 Reto 3: reemplaza en main.py el for que construye question_bank por una comprensión de listas (Día 26).
  • 🔴 Reto 4: escribe una prueba sin ventana: crea un QuizBrain con 3 preguntas y comprueba con assert que next_question() devuelve los textos correctos en orden.

5. Anotaciones de tipo (type hints) y la flecha ->

(Fuente: 265 - Python Typing- Type Hints and Arrows --.md)

🎯 Objetivo de aprendizaje

Declarar el tipo de variables, parámetros y valores de retorno para que el editor ayude a encontrar errores.

📖 Explicación conceptual

Python tiene tipado dinámico (Día 28): una variable puede cambiar de tipo. Las anotaciones de tipo permiten declarar qué tipo esperamos:

Python
age: int
name: str
height: float
is_human: bool

En funciones: el tipo de cada parámetro va después de :, y el tipo del valor de retorno, después de una flecha ->:

Python
def police_check(age: int) -> bool:
    if age > 18:
        can_drive = True
    else:
        can_drive = False
    return can_drive

Con esto, si escribes police_check("12") o haces que la función devuelva un texto, el editor (PyCharm, VS Code) subraya el error antes de ejecutar.

🛠️ Nota técnica importante: las anotaciones no las aplica Python al ejecutar el programa: son pistas para el editor y para las personas que leen el código. Verificado:

  • age: int seguido de age = "12" se ejecuta sin error; age queda como texto.
  • police_check("12") falla con TypeError: '>' not supported between instances of 'str' and 'int', pero por la comparación dentro de la función, no por la anotación.

El video da a entender que la anotación "provoca" el error; en realidad, lo que hace es avisarte antes.

🔍 Análisis del ejemplo: police_check(18) devuelve False (verificado), porque la condición es age > 18. Si la regla fuera "mayor de edad" (18 o más), habría que usar >=. Las anotaciones ayudan con los tipos, pero no detectan errores de lógica como este.

¿Desde cuándo existen? El curso dice "relativamente reciente": se introdujeron en Python 3.5 (2015) y se han ampliado en cada versión.

⚠️ Errores frecuentes

  • Creer que las anotaciones convierten los datos: def f(x: int) no convierte "5" en 5.
  • Confundir -> con =: la flecha indica el tipo de retorno, no asigna nada.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: agrega anotaciones a todos los métodos de QuizBrain: still_has_questions(self) -> bool, next_question(self) -> str, check_answer(self, user_answer: str) -> bool.
  • 🟡 Reto 2: escribe a propósito un error de tipo y comprueba que tu editor lo subraya. Toma una captura.
  • 🟠 Reto 3: anota una lista de preguntas: question_list: list[Question]. ¿Qué te sugiere el editor al escribir question_list[0].?
  • 🔴 Reto 4: investiga la herramienta mypy en su documentación oficial. Instálala y ejecútala sobre tu proyecto. ¿Qué encontró que Python no detectó al ejecutar?

6. Verificar la respuesta, dar retroalimentación y llevar el puntaje

(Fuentes: 266 - Check the Answer.md, 267 - Give Feedback to the Player, Keep Score and Fix the Bugs =).md)

📝 Ejercicio original del curso (parte 1: verificar)

Enunciado: al pulsar ✔ o ✘, llamar a check_answer del QuizBrain con la respuesta del usuario.

Solución del curso:

Python
self.true_button = Button(image=true_image, highlightthickness=0, command=self.true_pressed)
self.false_button = Button(image=false_image, highlightthickness=0, command=self.false_pressed)

def true_pressed(self):
    self.quiz.check_answer("True")

def false_pressed(self):
    self.quiz.check_answer("False")
  • command=self.true_pressed, sin paréntesis y con self. porque es un método de la clase.
  • Por ahora, check_answer imprime en la consola si acertaste: el usuario no lo ve.

📝 Ejercicio original del curso (parte 2: retroalimentación)

Enunciado: al responder, la tarjeta se pone verde si acertaste o roja si fallaste, y un segundo después aparece la siguiente pregunta.

🔍 Antes de programar

  • ¿Por qué no sirve time.sleep(1)? (Día 31)
  • check_answer imprime el resultado. ¿Qué debería hacer para que la interfaz lo pueda usar?

Solución del curso, paso a paso:

  1. check_answer ya no imprime: devuelve True o False (y sigue sumando el puntaje si acertaste).
  2. La interfaz pasa ese resultado a un método nuevo:
Python
def true_pressed(self):
    self.give_feedback(self.quiz.check_answer("True"))

def give_feedback(self, is_right):
    if is_right:
        self.canvas.config(bg="green")
    else:
        self.canvas.config(bg="red")
    self.window.after(1000, self.get_next_question)
  1. En get_next_question, la tarjeta vuelve a blanco y se actualiza el puntaje:
Python
def get_next_question(self):
    self.canvas.config(bg="white")
    if self.quiz.still_has_questions():
        self.score_label.config(text=f"Score: {self.quiz.score}")
        q_text = self.quiz.next_question()
        self.canvas.itemconfig(self.question_text, text=q_text)
    else:
        self.canvas.itemconfig(self.question_text, text="You've reached the end of the quiz.")
        self.true_button.config(state="disabled")
        self.false_button.config(state="disabled")

Los errores que el curso corrige al final:

  • En la pregunta 10, al pedir la siguiente: IndexError: list index out of range. Solución: still_has_questions() antes de avanzar.
  • Al terminar, los botones seguían cambiando el color de la tarjeta. Solución: state="disabled".
  • La tarjeta podía quedar roja o verde al final. Solución: volver a blanco fuera del if.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: cambia la duración de la retroalimentación a 600 ms con una constante FEEDBACK_MS.
  • 🟡 Reto 2: al final, muestra el puntaje en el lienzo: "Terminaste: 7 de 10".
  • 🟠 Reto 3: agrega un botón Jugar de nuevo que pida 10 preguntas nuevas y reinicie el puntaje, sin cerrar la ventana.
  • 🔴 Reto 4: permite responder con el teclado: T para verdadero y F para falso (self.window.bind). ¿Aparece algún problema nuevo? (Pista: sección 7, 🧪.)

7. 🚀 Proyecto del día: Quizzler completo

(Fuentes: 259 a 267)

Problema

Integrar la API, la interfaz con clases, la retroalimentación y el puntaje en una aplicación robusta.

Requisitos funcionales

  1. 10 preguntas de verdadero o falso nuevas en cada ejecución, sin entidades HTML.
  2. Ventana con puntaje, tarjeta con la pregunta y botones ✔ y ✘.
  3. Tarjeta verde o roja durante 1 segundo; luego la siguiente pregunta.
  4. Al terminar: mensaje final y botones desactivados.

Restricciones técnicas

Clases separadas (Question, QuizBrain, QuizInterface); after (nunca sleep); anotaciones de tipo en los métodos públicos.

Pruebas

Caso Resultado esperado
Abrir la aplicación Primera pregunta visible, "Score: 0"
Pregunta con comillas o apóstrofos Se ven correctamente
Responder bien Verde, puntaje +1
Responder mal Rojo, puntaje igual
Responder la pregunta 10 Mensaje final, botones desactivados, sin IndexError
Pulsar ✔ dos veces rápido ⚠️ (ver 🧪)
Abrir la aplicación sin internet ⚠️ (ver 🧪)
Abrir la aplicación dos veces seguidas ⚠️ (ver 🧪)

🧪 Prueba y depuración

  1. El doble clic (análisis de esta adaptación). Durante el segundo de retroalimentación, los botones siguen activos. Si pulsas ✔ dos veces rápido en una pregunta cuya respuesta es "True", se suman 2 puntos por una sola pregunta y se programan dos llamadas a get_next_question: la siguiente pregunta se salta sin responder (verificado con una simulación de la lógica: puntaje 2 y pregunta 2 nunca respondida). Corrígelo desactivando los botones en give_feedback y activándolos de nuevo en get_next_question.
  2. Sin internet. data.py hace la petición al importarse: sin conexión, el programa se cae con ConnectionError antes de abrir la ventana. Maneja el error y muestra un mensaje en la ventana (o usa el plan B del Reto 4 de la sección 1).
  3. Límite de peticiones (429). Guarda la última respuesta exitosa en preguntas.json y úsala si la API responde con error. Así también puedes desarrollar la interfaz sin hacer una petición en cada ejecución.
  4. Idioma. Las preguntas están en inglés. ¿Es un problema para tu grupo? Discute ventajas (práctica de inglés) y desventajas (comprensión) y propone una solución (sección 8).

⭐ Extensión del proyecto

  • Selector de categoría y dificultad al iniciar.
  • Récord guardado en un archivo (Día 24) y mostrado al terminar.
  • Temporizador de 10 segundos por pregunta con after: si se acaba, cuenta como incorrecta.

8. 🌟 Proyecto extra: el Concurso del Día de la Ciencia

Situación

Para el Día de la Ciencia, el colegio organiza un concurso de preguntas por equipos en el auditorio. Se proyecta una aplicación de selección múltiple con categorías científicas.

Requisitos

  1. Pantalla inicial con un menú para elegir la categoría (al menos 3 de la lista de Open Trivia DB, por ejemplo Ciencia y Naturaleza, Computadores y Matemáticas) y la dificultad.
  2. Preguntas de tipo multiple (4 opciones). Las opciones se mezclan al azar (random.shuffle) para que la correcta no quede siempre en el mismo lugar.
  3. Cuatro botones con las opciones; al responder, el botón correcto se pinta de verde y el elegido (si es incorrecto), de rojo.
  4. Dos equipos que se turnan; marcador para cada uno.
  5. Temporizador de 15 segundos por pregunta.
  6. Clases separadas: Pregunta, Concurso (lógica, turnos, puntajes) e InterfazConcurso.
  7. Banco propio en español: además de la API, un archivo preguntas_es.json con al menos 10 preguntas escritas por el grupo sobre ciencia en Colombia. El usuario elige "Preguntas del grupo (español)" o "Open Trivia DB (inglés)".
  8. Anotaciones de tipo en todos los métodos.

Decisiones que debes tomar

  • ¿Cómo evitas el problema del doble clic con cuatro botones?
  • ¿Las preguntas del banco propio tienen la misma estructura que las de la API? Si no, ¿dónde las conviertes?

Casos de prueba

Caso Esperado
La opción correcta aparece en posiciones distintas en varias preguntas Sí (mezcladas)
Se acaba el tiempo Cuenta como incorrecta; turno del otro equipo
Clic repetido en una opción Solo cuenta una vez
Preguntas con entidades HTML en las opciones Se ven limpias

9. 🏆 Proyecto integrador de los Días 33 y 34: el Tablero de Datos en Vivo

Proyecto integrador creado para esta adaptación. Combina APIs y parámetros (Día 33) con interfaces Tkinter basadas en clases, anotaciones de tipo y datos de una API en una interfaz (Día 34).

Situación problema

La sala de informática tiene un televisor que casi siempre está apagado. El grupo propone mostrar en él un tablero con datos en vivo que despierte la curiosidad de los estudiantes al entrar.

Pregunta orientadora

¿Cómo diseñar una aplicación gráfica, organizada en clases, que muestre datos de varias APIs, se actualice sola y siga funcionando cuando una fuente falla?

Objetivo

Construir el Tablero de Datos en Vivo: una ventana Tkinter con tres paneles que se actualizan automáticamente.

Requerimientos

  1. Panel ISS: latitud y longitud actuales, actualizadas cada 30 segundos, y un aviso si pasa a menos de 10 grados del colegio.
  2. Panel Sol: hora del amanecer y del atardecer del municipio (con el tzid correcto) y cuánto falta para el atardecer, actualizado cada minuto; la consulta a la API, una vez al día.
  3. Panel Pregunta del día: una pregunta de verdadero o falso de Open Trivia DB con botones; la respuesta se revela al pulsar. Una pregunta nueva cada 10 minutos.
  4. Una clase por fuente de datos (ServicioISS, ServicioSol, ServicioTrivia), cada una con un método que devuelve datos ya limpios o None si falló, con anotaciones de tipo.
  5. Una clase Tablero que solo muestra datos (no hace peticiones directamente).
  6. Si una fuente falla, su panel muestra "Sin datos (reintentando…)" y los demás siguen funcionando.
  7. Todas las peticiones con timeout y ninguna llamada a sleep.
  8. Configuración (coordenadas, intervalos, zona horaria) en un config.json.

Restricciones

  • Solo APIs sin clave vistas en los Días 33 y 34.
  • Respetar los límites de las APIs (nada de peticiones cada segundo).

Etapas de desarrollo

Etapa Entregable parcial
1. Diseño Boceto del tablero y diagrama de clases
2. Servicios Las tres clases de servicio probadas desde la consola
3. Interfaz Tablero con datos de prueba (sin internet)
4. Integración Paneles conectados a los servicios con after
5. Robustez Prueba desconectando internet durante 2 minutos
6. Presentación Tablero funcionando 30 minutos en la sala

Entregables

  • Código organizado en archivos (servicios, interfaz, main.py) y config.json.
  • Diagrama de clases.
  • 10 casos de prueba con resultados.
  • Registro de errores con al menos 5 errores reales.

Casos de prueba mínimos

Caso Esperado
Iniciar sin internet La ventana abre; los tres paneles dicen "Sin datos"
Internet vuelve Los paneles se llenan solos en el siguiente ciclo
La API de trivia responde 429 Solo ese panel muestra el aviso
30 minutos de funcionamiento No se congela; número de peticiones razonable

Criterios técnicos de funcionamiento

  • Tablero no importa requests.
  • Cambiar el municipio solo requiere editar config.json.
  • Las anotaciones de tipo no generan advertencias en el editor.

Posibles extensiones

Panel de clima (Día 35, con clave de API), modo de pantalla completa, registro de los pasos de la ISS en un CSV.


10. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Por qué main.py no tuvo que cambiar cuando question_data empezó a venir de la API?
  2. ¿Qué es una entidad HTML? Da dos ejemplos.
  3. ¿Por qué la interfaz recibe el QuizBrain como parámetro en lugar de crear uno nuevo?
  4. ¿Las anotaciones de tipo impiden que un programa se ejecute con tipos equivocados? Explica.
  5. ¿Qué significa el código HTTP 429?

Análisis de código

Python
self.window.after(1000, self.get_next_question)

¿Qué pasaría si escribieras self.get_next_question() con paréntesis?

Predicción

  1. ¿Qué imprime html.unescape("Rock &amp; Roll &quot;forever&quot;")?
  2. ```python def doble(n: int) -> int: return n * 2 print(doble("ja"))
Texto
   ¿Da error? ¿Qué imprime?

### Depuración

python class QuizInterface: def init(self, quiz_brain: QuizBrain): self.quiz = quiz_brain self.window = Tk() self.canvas = Canvas(width=300, height=250) self.question_text = self.canvas.create_text(text="...", font=("Arial", 20)) self.canvas.grid(row=1, column=0) self.window.mainloop() self.get_next_question() ``` Encuentra dos errores: uno produce una excepción y el otro hace que la primera pregunta nunca aparezca.

Modificación

Cambia la aplicación para que, al fallar, la tarjeta muestre durante el segundo de retroalimentación la respuesta correcta.

Creación

Escribe QuizBrain.porcentaje_aciertos(self) -> float que devuelva el porcentaje de aciertos sobre las preguntas respondidas hasta el momento, y muéstralo en la etiqueta del puntaje.


11. 🤖 Programar con apoyo de IA

Actividad 1: aprender a buscar (como enseña el curso). El curso resolvió las entidades HTML buscando en Google. Hazlo de las dos formas:

  1. Busca en un buscador "unescape HTML entities Python" y anota la primera solución.
  2. Pregunta lo mismo a una IA.
  3. Compara las dos respuestas con la documentación oficial del módulo html (se abre en otra pestaña). ¿Coinciden? ¿Cuál explicó mejor el porqué?

Actividad 2: la IA como revisora de errores de concurrencia. Pega tu QuizInterface y pregunta: "¿Qué pasa si el usuario pulsa el botón dos veces en menos de un segundo?". ¿Detecta el doble puntaje y la pregunta saltada? Compara con tu análisis de la sección 7 🧪.

Actividad 3: verificar una afirmación. Pregunta a una IA: "¿Python lanza un error si paso un str a un parámetro anotado como int?". Comprueba la respuesta ejecutando el código de la sección 5. Si la IA se equivocó, anota cómo lo descubriste.


12. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
261 Petición a Open Trivia DB sin manejo de errores Dos ejecuciones seguidas producen HTTP 429 con response_code: 5 (verificado); sin internet, ConnectionError al importar data.py Sección 1; 🧪 del proyecto
263 true_image y false_image como variables locales Funciona solo porque mainloop() está dentro de __init__; si se mueve, las imágenes desaparecen Nota técnica de la sección 3
265 La anotación de tipo hace que el código "dé error" Las anotaciones no se aplican al ejecutar: age: int acepta "12" (verificado); el TypeError viene de la comparación Nota técnica de la sección 5
265 police_check con age > 18 Con 18 años devuelve False (verificado): error de lógica que las anotaciones no detectan Sección 5
267 Retroalimentación con after(1000, ...) y botones activos Doble clic: puntaje doble y pregunta saltada (verificado con simulación) 🧪 del proyecto

13. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Objetivos: Quizzler, Open Trivia DB 259 Sección 0 — — Sección 7
Proyecto del Día 17, generador de URL, parámetros amount y type 260 Secciones 0 y 1 Desafío: preguntas desde la API Retos 1–4 de la sección 1 Secciones 7, 8 y 9
requests.get con params, data["results"] 261 Sección 1 Solución del desafío Retos 1–4 de la sección 1 Secciones 7, 8 y 9
Entidades HTML, html.unescape, buscar soluciones 262 Sección 2 Limpiar el texto Retos 1–4 de la sección 2 Secciones 7 y 8
Interfaz con clase, self., mainloop, lienzo, botones con imagen 263 Sección 3 Construir la interfaz Retos 1–4 de la sección 3 Secciones 7, 8 y 9
next_question devuelve texto, pasar QuizBrain, width del texto 264 Sección 4 Mostrar la pregunta Retos 1–4 de la sección 4 Secciones 7, 8 y 9
Anotaciones de tipo, ->, police_check 265 Sección 5 Ejemplo del curso Retos 1–4 de la sección 5 Secciones 8 y 9
true_pressed, false_pressed, check_answer 266 Sección 6 Verificar la respuesta Retos 1–4 de la sección 6 Sección 7
Retroalimentación con color, after, puntaje, fin del cuestionario, state="disabled", categoría 18 267 Sección 6 Retroalimentación y puntaje 🧪 del proyecto Secciones 7 y 8
(No existe en el curso original) — — — Concurso del Día de la Ciencia Sección 8
(No existe en el curso original) — — — Tablero de Datos en Vivo (Días 33–34) Sección 9

14. 📦 Evidencias de aprendizaje del Día 34

  • La respuesta JSON de Open Trivia DB impresa y tu explicación de response_code y results.
  • Una pregunta antes y después de html.unescape.
  • Quizzler funcionando: capturas de la tarjeta verde, roja y el mensaje final.
  • Captura del editor subrayando un error de tipo, y tu explicación de por qué Python igual lo ejecuta.
  • La corrección del doble clic y el plan B sin internet.
  • El Concurso del Día de la Ciencia (proyecto extra) o el Tablero de Datos en Vivo (proyecto integrador).
  • Cuaderno de errores: 429 por demasiadas peticiones, question_data mal nombrada, entidades HTML, create_text sin coordenadas, while que bloquea mainloop, código después de mainloop, IndexError al final del cuestionario, botones activos al terminar, doble clic y anotaciones que no se aplican al ejecutar.