Este tutorial adapta pedagógicamente el Día 31. 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 fragmentos de pandas y los errores se comprobaron con Python 3.14 y pandas 3.0. El proyecto inicial (las carpetas
imagesydata, confrench_words.csv) no está en la carpeta; el docente debe compartirlo o cada estudiante puede crear sus propios archivos (ver sección 0). Los programas con ventana se revisaron siguiendo la lógica del código y la documentación oficial de Tkinter (se abre en otra pestaña). Ejecútalos en tu computador para ver el resultado.
Tabla de contenidos
- Antes de empezar: el proyecto y la idea detrás de él
- Paso 1: la interfaz con Tkinter
- Paso 2: crear tarjetas nuevas con pandas
- Paso 3: voltear la tarjeta con
after - Paso 4: guardar el progreso
- 🚀 Proyecto del día: Flashy completo
- 🌟 Proyecto extra: el Mazo de Repaso para las Pruebas Saber 11
- 🏆 Proyecto integrador del nivel intermedio (Días 15 a 31): la Estación de Estudio
- 🧩 Comprueba lo aprendido
- 🤖 Programar con apoyo de IA
- 🛠️ Correcciones técnicas al material original
- 📋 Matriz de trazabilidad
- 📦 Evidencias de aprendizaje del Día 31
0. Antes de empezar: el proyecto y la idea detrás de él
(Fuente: 234 - Day 31 Goals- what you will make by the end of the day.md)
Hoy construiremos Flashy, una aplicación de tarjetas de estudio (flash cards) para aprender vocabulario de otro idioma:
- La tarjeta muestra una palabra en francés.
- A los 3 segundos, la tarjeta se voltea y muestra la traducción al inglés.
- Si ya sabías la palabra, pulsas ✅ y no vuelve a aparecer. Si no la sabías, pulsas ❌ y sigue en el mazo.
- El progreso se guarda: la próxima vez que abras la aplicación, solo verás las palabras que aún no sabes.
La idea pedagógica: el diccionario de frecuencias
La profesora del curso cuenta que estudió francés durante años memorizando listas y tablas gramaticales, y no aprendió a hablarlo. Lo que sí le funcionó fue una idea sencilla:
- En chino hay unos 50 000 caracteres, pero para leer textos sencillos basta con unos 1000.
- Lo importante es aprender primero las palabras más usadas. Un diccionario de frecuencias no ordena las palabras alfabéticamente, sino por cuántas veces aparecen en el uso real.
- Las listas que usa el curso se construyeron contando las palabras de los subtítulos de películas y series.
Para crear las tarjetas, la profesora copió 100 palabras frecuentes en una hoja de cálculo de Google y usó la fórmula =GOOGLETRANSLATE(celda, "fr", "en") para traducirlas. Luego descargó la hoja como CSV. Ese archivo, french_words.csv, ya viene en el proyecto inicial.
💡 Conexión: esta es una aplicación del análisis de datos que viste con pandas (Día 25): contar frecuencias y ordenar por ellas para decidir qué es más importante.
Materiales del proyecto
| Carpeta / archivo | Contenido |
|---|---|
images/ |
card_front.png (frente de la tarjeta), card_back.png (reverso), right.png (✅) y wrong.png (❌) |
data/french_words.csv |
100 palabras con las columnas French y English |
main.py |
Ya trae la constante BACKGROUND_COLOR con el color de fondo |
Si no tienes el proyecto inicial: crea un
data/french_words.csvcon al menos 10 palabras (primera línea:French,English). Para las imágenes, crea en cualquier editor un PNG de 800 × 526 para cada cara de la tarjeta y dos PNG pequeños (por ejemplo, 100 × 100) para los botones. También puedes hacer las tarjetas en inglés–español: solo cambian los nombres de las columnas.
Prerequisito general: Tkinter,
Canvas,PhotoImage,grid,columnspanyhighlightthickness(Días 27 y 28);window.afteryafter_cancel(Día 28, Pomodoro); pandas,read_csvyDataFrame(Día 25); excepciones yFileNotFoundError(Día 30); variables globales (Día 12);random.choice(Día 4).
1. Paso 1: la interfaz con Tkinter
(Fuentes: 235-step-1-create-the-user-interface-ui-with-tkinter.md, 236 - Solution & Walkthrough for Creating the UI.md)
🎯 Objetivo de aprendizaje
Construir una interfaz con un Canvas que superpone una imagen y dos textos, y botones con imágenes, organizados en una cuadrícula.
📝 Ejercicio original del curso
Enunciado: usar las imágenes de la carpeta images para construir la interfaz: una tarjeta con un título y una palabra, y debajo dos botones con imagen (❌ y ✅).
Pistas del curso:
- Una cuadrícula de 2 × 2, donde la tarjeta ocupa 2 columnas.
- La tarjeta es un
Canvascon 1 imagen y 2 textos. - La imagen es
card_front.png, creada conPhotoImage. Cuidado con la ruta: la imagen está dentro de la carpetaimages. - Para poner una imagen en un botón:
my_image = PhotoImage(file="path/to/image_file.png")
button = Button(image=my_image, highlightthickness=0)🔍 Antes de programar
- Dibuja la cuadrícula: ¿en qué fila y columna va cada elemento?
- ¿Por qué la tarjeta necesita un
Canvasy no unLabel? (Pista: ¿cuántas cosas van una encima de otra?) - La imagen de la tarjeta mide 800 × 526. ¿Cuál es el punto central?
📖 Explicación conceptual
¿Por qué un Canvas? Un Canvas (lienzo) permite superponer elementos: primero la imagen de la tarjeta y encima los dos textos. Cada elemento se ubica con coordenadas relativas al lienzo, no a la ventana.
Coordenadas en el lienzo: en Tkinter, el punto (0, 0) es la esquina superior izquierda y Y crece hacia abajo (al contrario de Turtle). Por eso el centro de un lienzo de 800 × 526 es (400, 263), y un texto en Y = 150 queda más arriba que el centro.
🔎 Ejemplo explicado (la solución del curso, reconstruida)
from tkinter import *
BACKGROUND_COLOR = "#B1DDC6" # usa el valor que trae el main.py del proyecto inicial
window = Tk()
window.title("Flashy")
window.config(padx=50, pady=50, bg=BACKGROUND_COLOR)
# La tarjeta: un lienzo del tamaño de la imagen
canvas = Canvas(width=800, height=526)
card_front_img = PhotoImage(file="images/card_front.png")
canvas.create_image(400, 263, image=card_front_img)
canvas.create_text(400, 150, text="Title", font=("Arial", 40, "italic"))
canvas.create_text(400, 263, text="word", font=("Arial", 60, "bold"))
canvas.config(bg=BACKGROUND_COLOR, highlightthickness=0)
canvas.grid(row=0, column=0, columnspan=2)
# Los botones
cross_image = PhotoImage(file="images/wrong.png")
unknown_button = Button(image=cross_image, highlightthickness=0)
unknown_button.grid(row=1, column=0)
check_image = PhotoImage(file="images/right.png")
known_button = Button(image=check_image, highlightthickness=0)
known_button.grid(row=1, column=1)
window.mainloop()Qué hace cada parte y por qué:
padx=50, pady=50: deja 50 píxeles de margen alrededor.create_image(400, 263, ...): la imagen se ubica por su centro, así que la ponemos en el centro del lienzo.bg=BACKGROUND_COLOR: la imagen tiene esquinas redondeadas y sombra; sin esto se vería un fondo blanco detrás.highlightthickness=0: quita el borde que Tkinter dibuja alrededor del lienzo y de los botones.columnspan=2: el lienzo ocupa las columnas 0 y 1. Sin esto, el botón ✅ queda lejos a la derecha, porque la columna 0 se vuelve tan ancha como el lienzo. El curso muestra este error y lo corrige.- El tamaño del texto de la palabra (60) es un ejemplo: la lección solo dice que es "más grande y en negrita" que el título.
Consejo del curso: al escribir rutas como
"images/card_front.png"es fácil cometer errores de escritura. Usa el autocompletado del editor: escribe la carpeta,/y elige el archivo.
Detalle visual: en Windows y Mac queda una línea fina alrededor de los botones aunque pongas
highlightthickness=0. El curso explica que es normal: le muestra al usuario que el botón se presionó.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
PhotoImage(file="card_front.png") sin la carpeta |
TclError: couldn't open "card_front.png" |
Escribe la ruta completa: "images/card_front.png" |
Crear un PhotoImage antes de Tk() |
RuntimeError: Too early to create image: no default root window (verificado) |
Crea la ventana primero |
Olvidar columnspan=2 |
El botón ✅ queda desplazado a la derecha | La tarjeta ocupa las dos columnas |
Ejecutar main.py desde otra carpeta |
La ruta relativa no encuentra las imágenes | Abre en el editor la carpeta del proyecto, o usa rutas con Path(__file__).parent (Día 24) |
| Confundir las coordenadas con las de Turtle | El título aparece abajo | En Tkinter, Y crece hacia abajo |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: agrega una etiqueta debajo de los botones que diga "¿Conocías esta palabra?".
- 🟡 Reto 2: agrega un tercer texto en el lienzo, en la parte inferior de la tarjeta, que más adelante mostrará cuántas palabras faltan.
- 🟠 Reto 3: reemplaza las rutas escritas a mano por rutas construidas con
Path(__file__).parent / "images" / "card_front.png". Ejecuta el programa desde otra carpeta y comprueba que sigue funcionando. - 🔴 Reto 4: diseña la interfaz para una pantalla pequeña (por ejemplo, un portátil de 1366 × 768). ¿Cabe la ventana? ¿Qué cambiarías: el tamaño del lienzo, las imágenes o el margen? Justifica tu decisión.
2. Paso 2: crear tarjetas nuevas con pandas
(Fuentes: 237-step-2-create-new-flash-cards.md, 238 - Solution & Walkthrough for Creating New Flash Cards.md)
🎯 Objetivo de aprendizaje
- Convertir un CSV en una lista de diccionarios con pandas.
- Elegir un elemento al azar y mostrarlo en el lienzo con
itemconfig.
📝 Ejercicio original del curso
Enunciado:
- Leer los datos de
data/french_words.csv. - Elegir una palabra francesa al azar y mostrarla en la tarjeta. Cada vez que se pulse ❌ o ✅, debe aparecer una palabra nueva al azar.
Pista del curso: usar pandas para crear un DataFrame y convertirlo en una lista de diccionarios con DataFrame.to_dict(orient="records").
🔍 Antes de programar
- ¿Qué forma tienen los datos del CSV? Abre el archivo y mira la primera línea.
- ¿Qué estructura es más cómoda para elegir "una fila al azar": un diccionario de columnas o una lista de filas?
- ¿Cómo cambias el texto de un elemento que ya existe en el lienzo?
📖 Explicación conceptual: las formas de to_dict
Con un CSV de tres palabras:
French,English
partie,part
histoire,history
chaque,eachForma por defecto (organizada por columnas):
data.to_dict()
# {'French': {0: 'partie', 1: 'histoire', 2: 'chaque'},
# 'English': {0: 'part', 1: 'history', 2: 'each'}}Con orient="records" (organizada por filas, una lista de diccionarios):
data.to_dict(orient="records")
# [{'French': 'partie', 'English': 'part'},
# {'French': 'histoire', 'English': 'history'},
# {'French': 'chaque', 'English': 'each'}](Ambos resultados verificados con pandas 3.0.)
La segunda forma es perfecta para tarjetas: cada diccionario es una tarjeta, con su frente ("French") y su reverso ("English"). Así, random.choice elige una tarjeta completa.
🛠️ Nota técnica: la pista de la lección
237dibuja el resultado como[{french_word: english_word}, ...], es decir, con la palabra francesa como clave. No es así: las claves son los nombres de las columnas,"French"y"English", como muestra el resultado verificado. La solución en video (238) sí lo usa correctamente:current_card["French"].
Cambiar un elemento del lienzo: create_text devuelve un identificador. Si lo guardas en una variable, después puedes modificarlo:
card_title = canvas.create_text(400, 150, text="", font=("Arial", 40, "italic"))
# ... más adelante:
canvas.itemconfig(card_title, text="French")🔎 La solución del curso (estructura)
import random
import pandas
data = pandas.read_csv("data/french_words.csv")
to_learn = data.to_dict(orient="records")
def next_card():
current_card = random.choice(to_learn)
canvas.itemconfig(card_title, text="French")
canvas.itemconfig(card_word, text=current_card["French"])- Los dos botones reciben
command=next_card(sin paréntesis: se pasa la función, no su resultado). - Se llama
next_card()una vez después de crear la interfaz y antes demainloop(), para que la primera tarjeta no muestre los textos de prueba "Title" y "word". - Las claves distinguen mayúsculas: el CSV dice
French, con F mayúscula.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
orient="record" (sin la s) |
ValueError: orient 'record' not understood (verificado) |
Escribe "records" |
current_card["french"] |
KeyError: 'french' |
Usa exactamente el nombre de la columna: "French" |
command=next_card() |
La función se ejecuta una vez al crear el botón y el botón no hace nada | Quita los paréntesis |
No llamar next_card() antes de mainloop |
La primera tarjeta dice "Title" y "word" | Llama la función al final de la interfaz |
ModuleNotFoundError: No module named 'pandas' |
pandas no está instalado en el intérprete del proyecto | Instálalo desde el editor o con pip install pandas |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: imprime
len(to_learn)al iniciar. ¿Coincide con el número de filas del CSV sin contar el encabezado? - 🟡 Reto 2: evita que la misma palabra salga dos veces seguidas.
- 🟠 Reto 3: crea tu propio CSV de 20 palabras inglés–español con vocabulario de tu clase de inglés y haz que la aplicación funcione con él. ¿Qué tuviste que cambiar en el código? ¿Podrías cambiarlo en un solo lugar?
- 🔴 Reto 4: en lugar de elegir totalmente al azar, haz que las palabras aparezcan en el orden del diccionario de frecuencias (primero las más usadas). ¿Qué ventaja y qué desventaja tiene frente al azar?
3. Paso 3: voltear la tarjeta con after
(Fuentes: 239-step-3-flip-the-cards.md, 240 - Solution & Walkthrough for Flipping Cards.md)
🎯 Objetivo de aprendizaje
- Programar una acción retrasada sin bloquear la interfaz, usando
window.after. - Cancelar un temporizador pendiente con
after_cancel. - Cambiar la imagen y el color de los elementos de un lienzo.
📝 Ejercicio original del curso
Enunciado:
- A los 3 segundos (3000 ms), la tarjeta se voltea y muestra la traducción al inglés de la palabra actual.
- La imagen cambia a
card_back.png, el texto se vuelve blanco y el título pasa de "French" a "English".
Pistas del curso:
- Para cambiar la imagen del lienzo necesitas guardar la referencia que devuelve
create_imagey luego usaritemconfig(..., image=nueva_imagen). - Importante: los objetos
PhotoImageno deben crearse dentro de una función. - El color del texto de un lienzo se cambia con el parámetro
fill. - Dentro de
mainloop()no usestime.sleep(); usawindow.after(). - Un
window.after()se puede cancelar conwindow.after_cancel().
🔍 Antes de programar
flip_cardnecesita saber qué tarjeta está en pantalla. ¿Dónde está guardada esa información ahora? ¿Puede verla otra función?- ¿Qué debe cambiar cuando se voltea la tarjeta? ¿Y qué debe volver a su estado inicial cuando aparece la siguiente?
- Si el usuario pulsa ✅ muy rápido cinco veces, ¿cuántos temporizadores quedan "contando"?
📖 Explicación conceptual
¿Por qué no time.sleep(3)? mainloop() es un bucle que atiende constantemente la ventana: clics, dibujos, teclado. sleep congela ese bucle: la ventana no responde durante 3 segundos. window.after(3000, flip_card) en cambio le dice a Tkinter: "dentro de 3000 ms, llama a flip_card", y mientras tanto la ventana sigue funcionando. Ya lo usaste en el Pomodoro (Día 28).
after devuelve un identificador. Si lo guardas, puedes cancelarlo:
flip_timer = window.after(3000, func=flip_card)
window.after_cancel(flip_timer)🔎 La solución del curso, construida por partes
Parte 1 — Compartir la tarjeta actual. current_card estaba "encerrada" dentro de next_card. El curso la saca como variable global que empieza como diccionario vacío:
current_card = {}
def next_card():
global current_card
current_card = random.choice(to_learn)
...
def flip_card():
canvas.itemconfig(card_title, text="English", fill="white")
canvas.itemconfig(card_word, text=current_card["English"], fill="white")
canvas.itemconfig(card_background, image=card_back_img)Parte 2 — Cambiar la imagen. Se guarda la referencia de la imagen del lienzo (card_background = canvas.create_image(...)) y se crea card_back_img fuera de las funciones, junto a card_front_img.
Parte 3 — Devolver la tarjeta al frente. En next_card hay que deshacer el volteo: título "French", texto negro (fill="black") e imagen card_front_img.
Parte 4 — Repetir el volteo en cada tarjeta. Si window.after solo se llama al crear la ventana, la primera tarjeta se voltea, pero las siguientes no. Por eso next_card también programa el volteo.
Parte 5 — El error de los temporizadores acumulados. El curso lo muestra: si pulsas los botones muchas veces, la tarjeta se voltea de inmediato. ¿Por qué? Cada after sigue contando en segundo plano, sin importar que ya cambiaste de tarjeta. La solución es cancelar el temporizador anterior antes de crear uno nuevo:
flip_timer = window.after(3000, func=flip_card) # justo después de crear la ventana
def next_card():
global current_card, flip_timer
window.after_cancel(flip_timer)
current_card = random.choice(to_learn)
# ... volver al frente y mostrar la palabra ...
flip_timer = window.after(3000, func=flip_card)🛠️ Nota técnica: la pista dice que un
PhotoImage"no debe crearse dentro de una función, o no funcionará". La causa real es que, al terminar la función, la variable local desaparece y Python libera la imagen (recolección de basura); Tkinter se queda mostrando un espacio vacío. Se puede crear dentro de una función si se conserva una referencia (por ejemplo, en una variable global o en un atributo de un objeto). La regla del curso es la forma más sencilla de evitar el problema.
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
Usar time.sleep(3) |
La ventana se congela | Usa window.after |
Olvidar global current_card en next_card |
flip_card muestra la tarjeta vacía del inicio: KeyError: 'English' |
Declara la variable como global |
Crear flip_timer después de la primera llamada a next_card() |
NameError al hacer after_cancel(flip_timer) |
Crea el primer temporizador justo después de la ventana |
No volver a fill="black" en next_card |
El texto blanco no se ve sobre el frente blanco | Restaura el color y la imagen del frente |
| No cancelar el temporizador | La tarjeta se voltea antes de tiempo | window.after_cancel(flip_timer) |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: convierte los 3000 ms en una constante
FLIP_DELAY_MSy prueba con 1500 y con 5000. ¿Cuál es mejor para estudiar? - 🟡 Reto 2: permite voltear la tarjeta manualmente haciendo clic sobre ella (investiga
canvas.bind("<Button-1>", ...)), además del volteo automático. - 🟠 Reto 3: muestra una cuenta regresiva (3, 2, 1) en una esquina de la tarjeta antes de voltear, usando
aftercada segundo como en el Pomodoro. - 🔴 Reto 4: reescribe la aplicación con una clase
FlashyAppen la quecurrent_card,flip_timery las imágenes sean atributos, sin ninguna variableglobal. ¿Qué ventajas tiene? (En el Día 34 el curso construye una interfaz Tkinter con clases.)
4. Paso 4: guardar el progreso
(Fuentes: 241-step-4-save-your-progress.md, 242 - Solution & Walkthrough for Saving Progress.md)
🎯 Objetivo de aprendizaje
- Eliminar elementos de una lista y guardar la lista como CSV.
- Elegir entre dos fuentes de datos según exista o no un archivo, usando
try/except/else.
📝 Ejercicio original del curso
Enunciado:
- Si el usuario pulsa ✅, la palabra actual se elimina de la lista de palabras que pueden salir.
- La lista actualizada se guarda en un archivo nuevo:
words_to_learn.csv. - Al iniciar, el programa revisa si existe
words_to_learn.csv. Si existe, usa esas palabras; si no (la primera vez), usafrench_words.csv.
Ejemplo del curso: si french_words.csv tuviera chaque,each, parlé,speak y arrivé,come, y el usuario marca que conoce parlé, words_to_learn.csv quedaría solo con chaque y arrivé.
Pistas del curso: list.remove(), DataFrame.to_csv() y el parámetro index=False.
🔍 Antes de programar
- ¿Qué hace ❌ ahora y qué debe hacer ✅? ¿Siguen llamando a la misma función?
- ¿En qué momento conviene guardar el archivo: al cerrar la aplicación o cada vez que se pulsa ✅? ¿Qué pasa si la aplicación se cierra de forma inesperada?
- ¿Qué línea puede fallar la primera vez que se ejecuta el programa?
🔎 La solución del curso, paso a paso
1. Una función nueva para ✅. El botón ❌ sigue con next_card; el ✅ ahora llama a is_known:
def is_known():
to_learn.remove(current_card)
data = pandas.DataFrame(to_learn)
data.to_csv("data/words_to_learn.csv", index=False)
next_card()remove busca en la lista un diccionario igual a current_card y elimina el primero que encuentra (verificado).
2. Elegir la fuente de datos. Es el patrón del Día 30:
try:
data = pandas.read_csv("data/words_to_learn.csv")
except FileNotFoundError:
original_data = pandas.read_csv("data/french_words.csv")
to_learn = original_data.to_dict(orient="records")
else:
to_learn = data.to_dict(orient="records")3. Guardar junto a los otros datos. La primera versión del curso guardaba words_to_learn.csv en la carpeta principal; luego lo movió a data/ para mantener el proyecto ordenado.
📖 El error de las columnas que se multiplican
El curso encontró un error "extraño": cada vez que se ejecutaba el programa, words_to_learn.csv tenía una columna más. Así se ve, paso a paso (verificado):
| Ejecución | Contenido de words_to_learn.csv |
|---|---|
1.ª (con to_csv sin index=False) |
,French,English / 0,partie,part / … |
| 2.ª (lee el archivo anterior y lo vuelve a guardar) | ,Unnamed: 0,French,English / 0,0,partie,part / … |
pandas guarda por defecto el índice (los números de fila) como primera columna. Al leer, esa columna se convierte en un dato más (Unnamed: 0), y al guardar se agrega otro índice. La solución: to_csv(..., index=False).
🛠️ Nota técnica: la pista 2 de la lección
241habla de crear un CSV "a partir de un diccionario". En realidad,to_learnes una lista de diccionarios;pandas.DataFrame(to_learn)la convierte en tabla (una fila por diccionario).
⚠️ Errores frecuentes
| Error | Qué ocurre | Solución |
|---|---|---|
Olvidar index=False |
Columnas Unnamed: 0 que crecen en cada ejecución |
Agrega index=False |
Conectar ✅ con next_card en vez de is_known |
Las palabras nunca se eliminan | Revisa el command de cada botón |
Leer siempre french_words.csv |
El progreso se pierde al reiniciar | Usa el try/except/else |
Guardar sobre french_words.csv |
Se pierden para siempre las palabras originales | Guarda en un archivo distinto |
Retos adicionales (creados para esta adaptación)
- 🟢 Reto 1: muestra en la ventana cuántas palabras quedan por aprender y actualízalo al pulsar ✅.
- 🟡 Reto 2: agrega un botón Reiniciar que borre
words_to_learn.csv(con confirmación,messagebox.askyesno) para empezar de cero. (Pista:Path.unlink()del Día 24.) - 🟠 Reto 3: además de las palabras que faltan, guarda en
data/learned.csvlas que ya aprendiste, con la fecha en que las marcaste. (Pista:datetime, que se estudia a fondo el Día 32.) - 🔴 Reto 4: ahora mismo, pulsar ❌ no cambia nada. Diseña un sistema en el que las palabras que fallas aparezcan más seguido (por ejemplo, contando fallos por palabra). ¿Qué columna nueva necesitaría el CSV?
5. 🚀 Proyecto del día: Flashy completo
(Fuentes: 234 a 242)
Problema
Integrar los cuatro pasos en una aplicación de estudio funcional, probada y robusta.
Contexto
El curso cierra con esta idea: la aplicación sirve para cualquier cosa que se estudie con "frente y reverso": idiomas, términos de programación, historia o geografía. Tú decides qué mazo construir.
Requisitos funcionales
- Tarjeta con título e idioma al frente; a los 3 segundos se voltea y muestra la traducción.
- ❌ muestra otra palabra; ✅ la elimina del mazo y muestra otra.
- El temporizador se reinicia con cada tarjeta nueva.
- El progreso se guarda en
data/words_to_learn.csvsin columnas de índice. - La primera ejecución usa
french_words.csvsin errores.
Restricciones técnicas
Tkinter con Canvas y grid; pandas para leer y guardar; window.after (nunca sleep); try/except/else para elegir el archivo; todos los PhotoImage fuera de las funciones.
Planificación
- Entradas: clics en ❌ y ✅, y los archivos CSV.
- Estado:
to_learn,current_card,flip_timer. - Salidas: la tarjeta en pantalla y
words_to_learn.csv. - Construye en el orden del curso: interfaz → tarjetas → volteo → progreso. Ejecuta después de cada paso.
Pruebas
| Caso | Resultado esperado |
|---|---|
Primera ejecución (sin words_to_learn.csv) |
Aparece una palabra en francés; no hay errores |
| Esperar 3 segundos | Tarjeta verde, título "English", texto blanco |
| Pulsar ❌ con la tarjeta volteada | Vuelve el frente, texto negro, palabra nueva |
| Pulsar ✅ cinco veces rápido | La tarjeta no se voltea antes de 3 s |
| Pulsar ✅ | Se crea o actualiza words_to_learn.csv con una fila menos |
| Cerrar y abrir | El número de palabras continúa donde quedó |
Abrir words_to_learn.csv |
Solo dos columnas: French,English |
| Marcar ✅ en todas las palabras | ⚠️ (ver 🧪) |
🧪 Prueba y depuración: ¿qué pasa cuando aprendes todo?
Usa un CSV con 3 palabras para probar rápido.
- Durante la sesión. Marca ✅ tres veces. Al eliminar la última,
next_card()ejecutarandom.choice([])y apareceIndexError: Cannot choose from an empty sequence(verificado). La ventana sigue abierta pero los botones ya no funcionan. - En la siguiente ejecución. Guardar una lista vacía produce un archivo que solo contiene un salto de línea. Al leerlo, pandas lanza
EmptyDataError: No columns to parse from file(verificado), y elexcept FileNotFoundErrordel curso no lo captura: la aplicación ni siquiera abre.
Actividad:
- Reproduce los dos errores y copia los mensajes en tu cuaderno.
- Decide qué debería pasar cuando el usuario aprende todas las palabras (¿un mensaje de felicitación?, ¿reiniciar el mazo?, ¿desactivar los botones?) e impleméntalo.
- Protege la lectura del archivo para que un
words_to_learn.csvvacío o dañado no impida abrir la aplicación. (Pista:pandas.errors.EmptyDataError.)
⭐ Extensión del proyecto
- Contador de palabras aprendidas y restantes.
- Teclas rápidas: ← para ❌ y → para ✅ (
window.bind). - Selector de mazo al iniciar (francés, inglés, términos de programación).
- Estadística al cerrar: cuántas palabras aprendiste en la sesión.
6. 🌟 Proyecto extra: el Mazo de Repaso para las Pruebas Saber 11
Situación
Los estudiantes de grado 11 del colegio se preparan para las Pruebas Saber 11. El grupo de décimo quiere dejarles una herramienta: una aplicación de tarjetas con varios mazos (por ejemplo, Ciencias Naturales, Sociales y Ciudadanas, e Inglés), escritos por los propios estudiantes.
Requisitos
- Un CSV por mazo, con las columnas
Pregunta,Respuesta(mínimo 15 tarjetas por mazo, redactadas y revisadas por el grupo). - Al iniciar, el usuario elige el mazo (con botones o un menú).
- Voltear con retraso, ❌ y ✅ como en Flashy.
- El progreso se guarda por mazo (un archivo de pendientes para cada uno).
- Un archivo
estadisticas.jsonguarda, por mazo, cuántas veces se respondió cada tarjeta con ❌ y con ✅. - Al terminar un mazo, un mensaje de felicitación y la opción de reiniciarlo.
- Ningún archivo faltante, vacío o dañado debe cerrar la aplicación.
Decisiones que debes tomar
- Las preguntas pueden ser más largas que una palabra. ¿Cómo haces que el texto quepa en la tarjeta? (Investiga el parámetro
widthdecreate_text.) - ¿El retraso para voltear debe ser igual para todos los mazos?
- ¿Guardas las estadísticas en JSON o en CSV? Justifica.
Casos de prueba
| Caso | Esperado |
|---|---|
| Elegir un mazo por primera vez | Se carga el CSV original del mazo |
| Aprender 3 tarjetas, cerrar y volver a ese mazo | Faltan 3 tarjetas menos; los otros mazos no cambian |
Borrar estadisticas.json |
La aplicación lo crea de nuevo sin errores |
| Terminar un mazo | Mensaje de felicitación, sin IndexError |
7. 🏆 Proyecto integrador del nivel intermedio (Días 15 a 31): la Estación de Estudio
Proyecto integrador creado para esta adaptación. Cierra el nivel intermedio del curso: programación orientada a objetos (Días 16 y 17), archivos y rutas (Día 24), CSV y pandas (Día 25), comprensión de listas y diccionarios (Día 26), Tkinter (Días 27 a 29), excepciones y JSON (Día 30) y el proyecto Flashy (Día 31).
Situación problema
Muchos estudiantes dicen que "estudian", pero no saben cuánto ni qué estudiaron. El comité académico del colegio propone una aplicación de escritorio que combine un temporizador Pomodoro con tarjetas de estudio y lleve un registro del tiempo y del progreso.
Pregunta orientadora
¿Cómo integrar en una sola aplicación bien organizada lo que aprendimos sobre clases, archivos, datos, interfaces gráficas y manejo de errores, para ayudar a un estudiante a estudiar mejor?
Objetivo
Construir la Estación de Estudio: una aplicación Tkinter que alterna bloques de estudio con tarjetas y descansos, y guarda estadísticas de cada sesión.
Requerimientos
- Clases propias (mínimo 3), por ejemplo:
Tarjeta,Mazo(carga, elimina y guarda tarjetas) yEstacionApp(la interfaz). Ninguna variableglobal. - Pomodoro: bloques de estudio y descanso con
after; durante el estudio se muestran tarjetas; durante el descanso, los botones se desactivan. - Mazos en CSV leídos con pandas; el progreso de cada mazo se guarda sin columnas de índice.
- Estadísticas en JSON: por fecha, minutos estudiados y tarjetas marcadas con ✅ y ❌.
- Al menos una comprensión de listas o diccionarios (por ejemplo, para calcular el porcentaje de aciertos por mazo).
- Rutas construidas con
pathlib(Día 24). - Manejo de
FileNotFoundError,EmptyDataErroryJSONDecodeError: la aplicación nunca se cierra por un archivo. - Una ventana de resumen con las estadísticas de los últimos 7 días.
Restricciones
- Solo bibliotecas vistas en el curso (Tkinter, pandas,
json,random,pathlib,datetimebásico). - Todos los valores configurables (minutos, retraso de volteo, colores) como constantes en mayúsculas.
Etapas de desarrollo
| Etapa | Entregable parcial |
|---|---|
| 1. Diseño | Boceto de la interfaz y diagrama de clases |
| 2. Datos | Clase Mazo probada sin interfaz (desde la consola) |
| 3. Interfaz | Tarjetas funcionando dentro de la clase EstacionApp |
| 4. Pomodoro | Alternancia de estudio y descanso |
| 5. Persistencia | Progreso en CSV y estadísticas en JSON |
| 6. Robustez | Todos los casos de archivos faltantes o dañados |
| 7. Presentación | Demostración y explicación del código a otro grupo |
Entregables
- Código en varios archivos (una clase por archivo como mínimo para
MazoyEstacionApp). - Diagrama de clases y boceto de la interfaz.
- Al menos 12 casos de prueba con resultados.
- Registro de errores con al menos 6 errores reales y su solución.
- Video corto de una sesión de estudio.
Casos de prueba mínimos
| Caso | Esperado |
|---|---|
| Primera ejecución sin archivos de progreso ni estadísticas | Se crean sin errores |
estadisticas.json vacío |
La aplicación abre y lo reinicia |
| Terminar un mazo | Mensaje; sin IndexError |
| Cerrar en mitad de un bloque y volver a abrir | El progreso de tarjetas se conserva |
| Cambiar la duración del bloque en una constante | El temporizador cambia sin modificar métodos |
Criterios técnicos de funcionamiento
- La clase
Mazose puede usar y probar sin la interfaz. - No hay código repetido para cargar o guardar archivos.
- Ninguna acción del usuario congela la ventana (no hay
sleep). - Otro estudiante puede agregar un mazo nuevo sin tocar el código, solo agregando un CSV.
Posibles extensiones
Gráfica de progreso semanal (con lo que se verá en días posteriores), sonido al terminar un bloque, envío del resumen por correo (Día 32).
8. 🧩 Comprueba lo aprendido
Conceptuales
- ¿Por qué la tarjeta usa un
Canvasy no unLabel? - ¿Qué diferencia hay entre
data.to_dict()ydata.to_dict(orient="records")? ¿Cuál conviene para elegir una tarjeta al azar y por qué? - ¿Por qué no se puede usar
time.sleep(3)para voltear la tarjeta? - ¿Qué es un "diccionario de frecuencias" y por qué ayuda a aprender un idioma?
Análisis de código
try:
data = pandas.read_csv("data/words_to_learn.csv")
except FileNotFoundError:
original_data = pandas.read_csv("data/french_words.csv")
to_learn = original_data.to_dict(orient="records")
else:
to_learn = data.to_dict(orient="records")¿Qué bloque se ejecuta la primera vez que se abre la aplicación? ¿Y la segunda? ¿Qué pasaría si se eliminara el else y su contenido?
Predicción
- Un usuario pulsa ✅ cuatro veces seguidas en menos de un segundo sin
after_cancel. ¿Cuántas veces se ejecutaráflip_carden los siguientes 3 segundos? - Un CSV con 100 palabras se guarda tres veces seguidas sin
index=False(leyendo el anterior cada vez). ¿Cuántas columnas tendrá el archivo final?
Depuración
def next_card():
current_card = random.choice(to_learn)
canvas.itemconfig(card_word, text=current_card["French"])
def flip_card():
canvas.itemconfig(card_word, text=current_card["English"])Con current_card = {} definido arriba, la tarjeta se voltea y el programa muestra un error. ¿Cuál? ¿Por qué? ¿Cómo se corrige?
def flip_card():
card_back_img = PhotoImage(file="images/card_back.png")
canvas.itemconfig(card_background, image=card_back_img)Al voltear, la tarjeta queda vacía. ¿Por qué?
Modificación
Cambia la aplicación para que la tarjeta muestre primero el inglés y al voltear el francés. ¿Cuántos lugares del código tuviste que modificar? ¿Cómo podrías lograrlo cambiando una sola constante?
Creación
Escribe una función porcentaje_aprendido(total_original, pendientes) que devuelva el porcentaje de palabras aprendidas redondeado a un decimal, y úsala para mostrar el progreso en la ventana.
9. 🤖 Programar con apoyo de IA
Actividad 1: crear el mazo con IA, verificarlo con criterio. La profesora usó Google Translate para traducir 100 palabras de una vez. Una IA también puede generar un CSV de vocabulario en segundos. Pero:
- Pide a una IA 20 palabras frecuentes en inglés con su traducción al español, en formato CSV.
- Revisa cada traducción con un diccionario confiable o con tu profesor de inglés. ¿Hay palabras con varios significados donde la IA eligió solo uno? ¿Hay errores?
- Comprueba que el CSV abre con pandas y que los nombres de las columnas son los que tu programa espera.
- Anota cuántas correcciones hiciste. ¿Confiarías en un mazo sin revisar para estudiar para un examen?
Actividad 2: comprobar una explicación técnica. Pregunta a una IA: "¿Por qué una imagen de Tkinter creada dentro de una función no se muestra?". Compara su respuesta con la nota técnica del Paso 3 y con la regla del curso. ¿Menciona la recolección de basura? ¿Propone guardar una referencia?
Actividad 3: la IA como probadora. Pega tu código final y pide: "Dame 5 casos de prueba que podrían romper esta aplicación". ¿Encuentra el caso de "aprender todas las palabras"? Ejecuta cada caso que proponga y registra si realmente rompe tu programa.
10. 🛠️ Correcciones técnicas al material original
| Archivo | Afirmación o comportamiento del curso | Observación | Tratamiento |
|---|---|---|---|
235 |
Enlaces a effbot.org para Canvas y PhotoImage |
Ese sitio ya no publica la documentación de Tkinter; usa la documentación oficial (se abre en otra pestaña) o TkDocs (se abre en otra pestaña) | Sección 1 |
237 |
El resultado de to_dict(orient="records") se dibuja como [{french_word: english_word}] |
Las claves son los nombres de las columnas: [{'French': ..., 'English': ...}] (verificado) |
Nota técnica de la sección 2 |
239 |
"Los PhotoImage no deben crearse dentro de una función, o no funcionará" |
La causa es la recolección de basura: funciona si se conserva una referencia | Nota técnica de la sección 3 |
241 |
Crear un CSV "a partir de un diccionario" | to_learn es una lista de diccionarios |
Nota técnica de la sección 4 |
242 |
Solo se captura FileNotFoundError |
Un words_to_learn.csv vacío produce EmptyDataError sin capturar (verificado) |
🧪 del proyecto |
242 |
next_card usa random.choice(to_learn) |
Al aprender la última palabra, IndexError por lista vacía (verificado) |
🧪 del proyecto |
236 |
Rutas relativas como "images/card_front.png" |
Dependen de la carpeta desde la que se ejecuta el programa | Errores frecuentes de la sección 1; Reto 3 |
11. 📋 Matriz de trazabilidad
| Contenido original | Archivo de origen | Concepto desarrollado | Ejercicio original | Reto adicional | Proyecto relacionado |
|---|---|---|---|---|---|
Objetivos, diccionario de frecuencias, GOOGLETRANSLATE, CSV |
234 |
Sección 0 | — | Actividad 1 de IA | Secciones 5 y 6 |
Interfaz: cuadrícula 2 × 2, Canvas, PhotoImage, botones con imagen |
235 |
Sección 1 | Paso 1 | Retos 1–4 de la sección 1 | Secciones 5, 6 y 7 |
Solución de la interfaz: columnspan, highlightthickness, rutas |
236 |
Sección 1 | Paso 1 | Retos 1–4 de la sección 1 | Secciones 5, 6 y 7 |
read_csv, to_dict(orient="records"), random.choice |
237 |
Sección 2 | Paso 2 | Retos 1–4 de la sección 2 | Secciones 5, 6 y 7 |
next_card, itemconfig, llamar antes de mainloop |
238 |
Sección 2 | Paso 2 | Retos 1–4 de la sección 2 | Secciones 5, 6 y 7 |
after, fill, PhotoImage fuera de funciones, after_cancel |
239 |
Sección 3 | Paso 3 | Retos 1–4 de la sección 3 | Secciones 5, 6 y 7 |
global current_card, cambiar imagen, flip_timer |
240 |
Sección 3 | Paso 3 | Retos 1–4 de la sección 3 | Secciones 5, 6 y 7 |
remove, to_csv, index=False, archivo de progreso |
241 |
Sección 4 | Paso 4 | Retos 1–4 de la sección 4 | Secciones 5, 6 y 7 |
is_known, try/except/else, columnas Unnamed: 0 |
242 |
Sección 4 | Paso 4 | 🧪 del proyecto | Secciones 5, 6 y 7 |
| (No existe en el curso original) | — | — | — | Mazo de Repaso Saber 11 | Sección 6 |
| (No existe en el curso original) | — | — | — | Estación de Estudio (Días 15–31) | Sección 7 |
12. 📦 Evidencias de aprendizaje del Día 31
- El boceto de la cuadrícula y la interfaz funcionando (captura).
- El resultado impreso de
to_dict()y deto_dict(orient="records")con tu CSV, y tu explicación de la diferencia. - Un video corto o capturas que muestren el volteo y la prueba de pulsar ✅ rápido varias veces.
words_to_learn.csvantes y después de marcar varias palabras (sin columnas de índice).- La solución a los dos errores de la sección 🧪 (aprender todas las palabras).
- Tu propio mazo (inglés–español, Saber 11 u otro tema) con las traducciones revisadas.
- Cuaderno de errores: rutas de imágenes,
columnspanolvidado,orient="record",KeyErrorpor mayúsculas,globalolvidado, imagen que desaparece dentro de una función, temporizadores acumulados, columnasUnnamed: 0,IndexErrorcon lista vacía yEmptyDataError.