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
requests2.34. La lógica del cuestionario se verificó con una réplica de las clases del Día 17. El proyecto inicial (imágenestrue.pngyfalse.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
- Antes de empezar: del cuestionario de consola a Quizzler
- Preguntas nuevas en cada ejecución con Open Trivia DB
- Entidades HTML y
html.unescape - Una interfaz Tkinter dentro de una clase
- Mostrar la siguiente pregunta en la interfaz
- Anotaciones de tipo (type hints) y la flecha
-> - Verificar la respuesta, dar retroalimentación y llevar el puntaje
- 🚀 Proyecto del día: Quizzler completo
- 🌟 Proyecto extra: el Concurso del Día de la Ciencia
- 🏆 Proyecto integrador de los Días 33 y 34: el Tablero de Datos en Vivo
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 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 yraise_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í:
https://opentdb.com/api.php?amount=10&type=booleanRecuerda (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.pysiga 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
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):
{
"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):
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.jsonla 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
amounta 5 y ejecuta el cuestionario de consola. - 🟡 Reto 2: agrega el parámetro
categorycon el número 18 (Science: Computers, verificado enhttps://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: "glace" o Stalin'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 <.
| Entidad | Carácter |
|---|---|
" |
" |
' |
' |
& |
& |
< / > |
< / > |
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.
import html
print(html.unescape("The Cold War ended with Joseph Stalin'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:
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
unescapeendata.py, una sola vez al recibir los datos, en lugar de cada vez que se muestra una pregunta. AsíQuizBrainrecibe 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.unescapea"Tom & Jerry <3"y predice el resultado antes de ejecutarlo. - 🟡 Reto 2: el proceso inverso existe: investiga
html.escapey úsalo con un texto que tenga<y&. - 🟠 Reto 3: mueve el
unescapeadata.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 antiguowhiledel cuestionario de consola se comenta:mainloopya es un bucle infinito que atiende la ventana, y otrowhilelo 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.pngyfalse.png.
🔎 La solución del curso (reconstruida)
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:
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 produjoIndexError: 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_imageyfalse_imageno llevanself.porque solo se usan dentro de__init__.
🛠️ Nota técnica (análisis de esta adaptación): que
true_imagesea una variable local funciona solo porquemainloop()está dentro de__init__: el método no termina mientras la ventana está abierta, y la imagen sigue existiendo. Si movierasmainloop()fuera de__init__(por ejemplo, amain.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 comoself.true_imageevita 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()amain.py(quiz_ui.window.mainloop()). ¿Qué pasa con las imágenes? Corrígelo conself.. - 🟠 Reto 3: si no tienes las imágenes, crea los botones con
text="✔"ytext="✘", 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:
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:
# main.py
quiz = QuizBrain(question_bank)
quiz_ui = QuizInterface(quiz)# 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: QuizBraines una anotación de tipo (sección 5): le dice al editor qué clase de objeto llega. Gracias a eso, al escribirself.quiz.el editor sugiere los métodos deQuizBrain. Sin ella, no sugería nada.get_next_question()se llama en__init__antes demainloop(), para que la primera pregunta aparezca al abrir.- Texto que no cabe: el parámetro
width=280encreate_texthace 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
Questiony endata.py? - 🟠 Reto 3: reemplaza en
main.pyelforque construyequestion_bankpor una comprensión de listas (Día 26). - 🔴 Reto 4: escribe una prueba sin ventana: crea un
QuizBraincon 3 preguntas y comprueba conassertquenext_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:
age: int
name: str
height: float
is_human: boolEn funciones: el tipo de cada parámetro va después de :, y el tipo del valor de retorno, después de una flecha ->:
def police_check(age: int) -> bool:
if age > 18:
can_drive = True
else:
can_drive = False
return can_driveCon 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: intseguido deage = "12"se ejecuta sin error;agequeda como texto.police_check("12")falla conTypeError: '>' 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)devuelveFalse(verificado), porque la condición esage > 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"en5. - 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 escribirquestion_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:
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 conself.porque es un método de la clase.- Por ahora,
check_answerimprime 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_answerimprime el resultado. ¿Qué debería hacer para que la interfaz lo pueda usar?
Solución del curso, paso a paso:
check_answerya no imprime: devuelveTrueoFalse(y sigue sumando el puntaje si acertaste).- La interfaz pasa ese resultado a un método nuevo:
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)- En
get_next_question, la tarjeta vuelve a blanco y se actualiza el puntaje:
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:
Tpara verdadero yFpara 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
- 10 preguntas de verdadero o falso nuevas en cada ejecución, sin entidades HTML.
- Ventana con puntaje, tarjeta con la pregunta y botones ✔ y ✘.
- Tarjeta verde o roja durante 1 segundo; luego la siguiente pregunta.
- 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
- 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 engive_feedbacky activándolos de nuevo enget_next_question. - Sin internet.
data.pyhace la petición al importarse: sin conexión, el programa se cae conConnectionErrorantes 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). - Límite de peticiones (429). Guarda la última respuesta exitosa en
preguntas.jsony úsala si la API responde con error. Así también puedes desarrollar la interfaz sin hacer una petición en cada ejecución. - 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
- 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.
- 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. - Cuatro botones con las opciones; al responder, el botón correcto se pinta de verde y el elegido (si es incorrecto), de rojo.
- Dos equipos que se turnan; marcador para cada uno.
- Temporizador de 15 segundos por pregunta.
- Clases separadas:
Pregunta,Concurso(lógica, turnos, puntajes) eInterfazConcurso. - Banco propio en español: además de la API, un archivo
preguntas_es.jsoncon 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)". - 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
- Panel ISS: latitud y longitud actuales, actualizadas cada 30 segundos, y un aviso si pasa a menos de 10 grados del colegio.
- Panel Sol: hora del amanecer y del atardecer del municipio (con el
tzidcorrecto) y cuánto falta para el atardecer, actualizado cada minuto; la consulta a la API, una vez al día. - 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.
- Una clase por fuente de datos (
ServicioISS,ServicioSol,ServicioTrivia), cada una con un método que devuelve datos ya limpios oNonesi falló, con anotaciones de tipo. - Una clase
Tableroque solo muestra datos (no hace peticiones directamente). - Si una fuente falla, su panel muestra "Sin datos (reintentando…)" y los demás siguen funcionando.
- Todas las peticiones con
timeouty ninguna llamada asleep. - 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) yconfig.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
Tablerono importarequests.- 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
- ¿Por qué
main.pyno tuvo que cambiar cuandoquestion_dataempezó a venir de la API? - ¿Qué es una entidad HTML? Da dos ejemplos.
- ¿Por qué la interfaz recibe el
QuizBraincomo parámetro en lugar de crear uno nuevo? - ¿Las anotaciones de tipo impiden que un programa se ejecute con tipos equivocados? Explica.
- ¿Qué significa el código HTTP 429?
Análisis de código
self.window.after(1000, self.get_next_question)¿Qué pasaría si escribieras self.get_next_question() con paréntesis?
Predicción
- ¿Qué imprime
html.unescape("Rock & Roll "forever"")? - ```python def doble(n: int) -> int: return n * 2 print(doble("ja"))
¿Da error? ¿Qué imprime?
### Depuraciónpython 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:
- Busca en un buscador "unescape HTML entities Python" y anota la primera solución.
- Pregunta lo mismo a una IA.
- 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_codeyresults. - 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_datamal nombrada, entidades HTML,create_textsin coordenadas,whileque bloqueamainloop, código después demainloop,IndexErroral final del cuestionario, botones activos al terminar, doble clic y anotaciones que no se aplican al ejecutar.