Saltar al contenido
Inicio 100 Días de Código en Python — Día 32: Enviar correos (smtplib) y manejar fechas (datetime)

100 Días de Código en Python — Día 32: Enviar correos (smtplib) y manejar fechas (datetime)

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

Este tutorial adapta pedagógicamente el Día 32. Las secciones 📝 Ejercicio original del curso provienen de los archivos del curso. Los retos 🟢🟡🟠🔴, el proyecto extra y el proyecto integrador son creaciones de esta adaptación. Las correcciones al material original aparecen como 🛠️ Nota técnica.

Nota sobre las verificaciones: los ejemplos se ejecutaron con Python 3.14 y pandas 3.0. El envío de correos se probó con un servidor de correo local (aiosmtpd 1.4.6, ver sección 1), no con cuentas reales. Los archivos iniciales del curso (quotes.txt, birthdays.csv y las plantillas de cartas) no están en la carpeta; el tutorial explica cómo crearlos.

🔐 Seguridad y ética (adaptación para el aula): en este día se trabaja con contraseñas y datos personales (nombres, correos, fechas de nacimiento). Reglas para la clase:

  1. Practica con el servidor local de la sección 1. Si el docente autoriza el envío real, usa solo una cuenta de prueba creada para eso, nunca tu correo personal ni el de tu familia.
  2. Nunca subas una contraseña a un repositorio, un documento compartido o un chat.
  3. Usa datos inventados. En Colombia, los datos personales están protegidos por la Ley 1581 de 2012 (habeas data): para guardar y usar los datos de otra persona se necesita su autorización.

Tabla de contenidos

  1. Antes de empezar
  2. Enviar correos con smtplib
  3. Fechas y horas con datetime
  4. Desafío 1: citas motivacionales los lunes
  5. 🚀 Proyecto del día: el Felicitador Automático de Cumpleaños
  6. Ejecutar el programa todos los días en la nube
  7. 🌟 Proyecto extra: el Boletín Diario de Tareas del Curso
  8. 🏆 Proyecto integrador de los Días 24 a 32: el Asistente del Director de Grupo
  9. 🧩 Comprueba lo aprendido
  10. 🤖 Programar con apoyo de IA
  11. 🛠️ Correcciones técnicas al material original
  12. 📋 Matriz de trazabilidad
  13. 📦 Evidencias de aprendizaje del Día 32

0. Antes de empezar

(Fuente: 243 - Day 32 Goals- what we will make by the end of the day.md)

Hoy aprenderemos dos módulos que ya vienen con Python (no hay que instalarlos):

  • smtplib: enviar correos electrónicos desde un programa.
  • datetime: saber la fecha y hora actuales, y crear y comparar fechas.

Con ellos construiremos un felicitador automático de cumpleaños: una hoja de cálculo con los cumpleaños de amigos y familiares, varias cartas de felicitación y un programa que, cada día, revisa si alguien cumple años y le envía una carta elegida al azar.

La profesora del curso cuenta que depende de Facebook para recordar cumpleaños. El programa resuelve ese problema… aunque también plantea una pregunta ética que ella misma deja abierta: ¿es sincero un saludo automático? Discútanlo en clase (sección 4).

Prerequisito general: archivos y with open (Día 24); CSV, pandas e iterrows (Días 25 y 26); comprensión de diccionarios (Día 26); tuplas como claves de diccionario (Día 26); random (Día 4); replace de cadenas; excepciones (Día 30).


1. Enviar correos con smtplib

(Fuentes: 244-a-note-about-the-next-lesson-google-smtp-port.md, 245 - How to Send Emails with Python using SMTP.md)

🎯 Objetivo de aprendizaje

  • Explicar qué es SMTP y cómo viaja un correo.
  • Enviar un correo con asunto y cuerpo desde Python.
  • Diagnosticar los errores más comunes de conexión.

📖 Explicación conceptual: ¿cómo viaja un correo?

Cuando angela@gmail.com escribe a timmy@yahoo.com:

Texto
Programa de Angela ──► Servidor de Gmail ──► Servidor de Yahoo ──► Computador de Timmy
                   SMTP                 SMTP                  (Timmy descarga el correo)

SMTP (Simple Mail Transfer Protocol, protocolo simple de transferencia de correo) es el conjunto de reglas con las que los servidores reciben y reenvían correos. La analogía del curso: los servidores son oficinas de correos, el computador de Timmy es su buzón y SMTP es el cartero.

📖 Los cinco pasos del envío

Paso Código Para qué
1. Conectarse smtplib.SMTP("smtp.gmail.com", port=587) Abrir una conexión con el servidor de tu proveedor
2. Asegurar connection.starttls() TLS cifra la conexión: si alguien intercepta el mensaje, no lo puede leer
3. Iniciar sesión connection.login(user=..., password=...) Demostrar que la cuenta es tuya
4. Enviar connection.sendmail(from_addr=..., to_addrs=..., msg=...) Entregar el mensaje
5. Cerrar connection.close() o usar with Liberar la conexión

La solución del curso (con with, que cierra la conexión solo):

Python
import smtplib

my_email = "cuenta_de_prueba@gmail.com"
password = "contraseña_de_aplicación"

with smtplib.SMTP("smtp.gmail.com", port=587) as connection:
    connection.starttls()
    connection.login(user=my_email, password=password)
    connection.sendmail(
        from_addr=my_email,
        to_addrs="destinatario_de_prueba@yahoo.com",
        msg="Subject:Hello\n\nThis is the body of my email."
    )

El asunto y el cuerpo: un correo sin asunto tiene muchas probabilidades de ir a spam. En sendmail, el mensaje empieza con Subject: y el asunto; después van dos saltos de línea (\n\n) y luego el cuerpo.

📖 La configuración según el proveedor (lección 244)

  1. Dirección SMTP: cada proveedor tiene la suya. Gmail: smtp.gmail.com; Yahoo: smtp.mail.yahoo.com. Para otros, busca "[proveedor] SMTP settings" en la ayuda oficial del proveedor.
  2. Contraseña de aplicación (Gmail): activar la verificación en 2 pasos en la cuenta de Google, buscar App Passwords, crear una con un nombre (por ejemplo, "Python Mail") y copiarla: solo se muestra una vez. Son 16 caracteres. En el código se usa esa contraseña, no la normal. Yahoo tiene un proceso parecido en su página de seguridad.
  3. Puerto: por defecto smtplib.SMTP usa el puerto 25, que hoy casi todos los servidores bloquean por abuso. Para conexiones desde un programa se usa normalmente el 587: smtplib.SMTP("smtp.gmail.com", port=587).

🛠️ Nota técnica (información desactualizada): la lección 244 indica smtp.live.com para Hotmail y outlook.office365.com para Outlook. Esa segunda dirección corresponde a la lectura del correo (IMAP/POP), no al envío, y Microsoft ha cambiado varias veces la forma de autenticar cuentas personales, por lo que un programa sencillo con usuario y contraseña puede no funcionar. Además, el video 245 habla de hacer las cuentas "menos seguras"; Google ya no ofrece esa opción, y el camino actual es la contraseña de aplicación. Verifica siempre en la ayuda oficial del proveedor.

🏫 Alternativa para el aula: un servidor de correo local (adaptación)

Para aprender cómo funciona smtplib no hace falta enviar correos reales. Existe una biblioteca, aiosmtpd, que crea en tu computador un servidor de correo de práctica: recibe los mensajes y los muestra en la consola, sin enviarlos a nadie.

  1. Instálala: pip install aiosmtpd.
  2. En una terminal, inicia el servidor y déjalo abierto:
Texto
python -m aiosmtpd -n -l localhost:8025
  1. En tu programa, conéctate a él. Este servidor no usa cifrado ni contraseñas, así que se omiten starttls() y login():
Python
import smtplib

with smtplib.SMTP("localhost", port=8025) as connection:
    connection.sendmail(
        from_addr="yo@colegio.test",
        to_addrs="tu@colegio.test",
        msg="Subject:Prueba\n\nHola desde Python"
    )
  1. Mira la terminal del servidor: aparece el correo completo entre MESSAGE FOLLOWS y END MESSAGE (verificado).

Cuando tu programa funcione con el servidor local, pasar a una cuenta real solo requiere cambiar la dirección, el puerto y volver a poner starttls() y login().

⚠️ Errores frecuentes

Error Qué ocurre Solución
SMTPAuthenticationError Usuario o contraseña rechazados Usa una contraseña de aplicación, no la normal
SMTPServerDisconnected: Connection unexpectedly closed Dirección SMTP equivocada, puerto equivocado o cuenta mal configurada Revisa los tres puntos de la lección 244
Un error de escritura en el correo, la contraseña o la dirección SMTP Fallos difíciles de ver Revisa cada cadena letra por letra, como insiste el curso
El correo no llega Probablemente está en spam Revisa spam; agrega un asunto
UnicodeEncodeError: 'ascii' codec can't encode character '\xf1' El mensaje tiene ñ, tildes o signos como ¡ (verificado) Ver la nota técnica siguiente
SMTPNotSupportedError: STARTTLS extension not supported Llamaste starttls() en el servidor local (verificado) Con el servidor local, omite starttls() y login()

🛠️ Nota técnica (importante en español): sendmail convierte el mensaje a ASCII, que no incluye ñ, tildes ni ¡¿. Por eso msg="Subject:Feliz cumpleaños\n\n¡Hola, Mamá!" produce UnicodeEncodeError (verificado). El curso no lo menciona porque sus mensajes están en inglés. La solución moderna es construir el mensaje con EmailMessage y enviarlo con send_message (verificado con el servidor local):

Python
from email.message import EmailMessage

msg = EmailMessage()
msg["Subject"] = "Feliz cumpleaños"
msg["From"] = "yo@colegio.test"
msg["To"] = "tu@colegio.test"
msg.set_content("¡Hola, Mamá! Que tengas un día increíble.")

with smtplib.SMTP("localhost", port=8025) as connection:
    connection.send_message(msg)

Además de aceptar cualquier carácter, así no hay que armar a mano el Subject: con \n\n.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: con el servidor local, envía un correo con asunto y un cuerpo de tres líneas. Copia lo que muestra el servidor e identifica el asunto, el remitente y el cuerpo.
  • 🟡 Reto 2: envía el mismo correo a tres destinatarios en una sola llamada. (Pista: to_addrs acepta una lista.)
  • 🟠 Reto 3: reescribe el Reto 1 con EmailMessage y un asunto con tildes. Compara, en la consola del servidor, cómo se ve el asunto (=?utf-8?q?...). ¿Por qué crees que se codifica así?
  • 🔴 Reto 4: escribe una función enviar_correo(destino, asunto, cuerpo, servidor, puerto) que capture SMTPAuthenticationError, SMTPServerDisconnected y ConnectionRefusedError (servidor local apagado) y muestre un mensaje claro para cada uno.

2. Fechas y horas con datetime

(Fuente: 246 - Working with the datetime Module.md)

🎯 Objetivo de aprendizaje

Obtener la fecha y hora actuales, extraer sus partes y crear fechas propias.

📖 Explicación conceptual

El módulo datetime tiene una clase que también se llama datetime, lo que confunde. El curso usa un alias:

Python
import datetime as dt

now = dt.datetime.now()
print(now)            # por ejemplo: 2026-09-26 10:15:42.123456
print(type(now))      # <class 'datetime.datetime'>

now es un objeto, no un texto. Por eso podemos pedirle sus partes como atributos, que son números enteros:

Python
year = now.year       # int
month = now.month
day = now.day
hour = now.hour
if year == 2026:
    print("Estamos en 2026")

Día de la semana: now.weekday() es un método (lleva paréntesis) que devuelve un número. Como los computadores cuentan desde cero: 0 = lunes, 1 = martes, …, 6 = domingo (verificado: el 28 de septiembre de 2026 es lunes y weekday() devuelve 0).

Crear una fecha propia: año, mes y día son obligatorios; hora, minuto y segundo tienen valores por defecto (cero):

Python
date_of_birth = dt.datetime(year=1995, month=12, day=15)
print(date_of_birth)                       # 1995-12-15 00:00:00
date_of_birth = dt.datetime(year=1995, month=12, day=15, hour=4)
print(date_of_birth)                       # 1995-12-15 04:00:00

(Resultados verificados.)

Alternativa de importación (la usa el curso en el proyecto): from datetime import datetime y luego datetime.now(). Las dos formas son válidas; elige una y úsala de forma consistente.

⚠️ Errores frecuentes

Error Qué ocurre Solución
now.weekday sin paréntesis Obtienes el método, no el número Escribe now.weekday()
Creer que el lunes es 1 El programa se ejecuta el día equivocado Lunes = 0
dt.now() AttributeError: el módulo no tiene now dt.datetime.now()
dt.datetime(2027, 2, 29) ValueError: day 29 must be in range 1..28 for month 2 in year 2027 (verificado) Valida las fechas; 2027 no es bisiesto

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: imprime la fecha de hoy con el formato 26/09/2026 usando los atributos.
  • 🟡 Reto 2: imprime el nombre del día de la semana en español usando weekday() y una lista.
  • 🟠 Reto 3: pide tu fecha de nacimiento y calcula cuántos años cumples este año. ¿Ya los cumpliste?
  • 🔴 Reto 4: investiga timedelta en la documentación oficial (se abre en otra pestaña) y calcula cuántos días faltan para tu próximo cumpleaños. Prueba con alguien que haya nacido un 29 de febrero.

3. Desafío 1: citas motivacionales los lunes

(Fuente: 247 - Challenge 1 - Send Motivational Quotes on Mondays via Email.md)

📝 Ejercicio original del curso

Enunciado: si hoy es lunes, enviarte un correo con una cita motivacional al azar tomada de quotes.txt (una cita por línea, con su autor).

Truco del curso para probar: para no esperar al lunes, compara con el día de hoy mientras pruebas (por ejemplo, weekday == 1 si es martes). Cuando todo funcione, cámbialo a 0.

Si no tienes quotes.txt: créalo con al menos 5 frases, una por línea, con el formato "Frase" - Autor. Pueden ser frases de personajes colombianos o frases escritas por el curso.

🔍 Antes de programar

  • ¿Qué información necesitas antes de decidir si envías el correo?
  • ¿Cómo obtienes una lista de las líneas de un archivo? (Día 24)
  • ¿Quién es el remitente y quién el destinatario?

🔎 La solución del curso (estructura)

Python
import smtplib
import datetime as dt
import random

MY_EMAIL = "cuenta_de_prueba@gmail.com"
MY_PASSWORD = "contraseña_de_aplicación"

now = dt.datetime.now()
weekday = now.weekday()
if weekday == 0:
    with open("quotes.txt") as quote_file:
        all_quotes = quote_file.readlines()
        quote = random.choice(all_quotes)

    with smtplib.SMTP("smtp.gmail.com", port=587) as connection:
        connection.starttls()
        connection.login(MY_EMAIL, MY_PASSWORD)
        connection.sendmail(
            from_addr=MY_EMAIL,
            to_addrs=MY_EMAIL,
            msg=f"Subject:Monday Motivation\n\n{quote}"
        )
  • readlines() devuelve una lista con una línea por elemento.
  • La variable quote se crea dentro del with, pero sigue existiendo después: en Python, with no crea un ámbito nuevo.
  • El remitente y el destinatario son el mismo correo: te envías la cita a ti mismo.

⚠️ Errores frecuentes

  • Olvidar volver a poner 0 después de las pruebas: recibirás citas los martes.
  • Las citas copiadas de internet suelen tener comillas tipográficas (“ ”) o rayas (—). Con sendmail, eso produce UnicodeEncodeError (nota técnica de la sección 1). Usa EmailMessage o comillas rectas.
  • open("quotes.txt") sin encoding="utf-8" puede leer mal las tildes en Windows. Agrégalo si tus citas están en español.

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: prueba el programa con el servidor local y muestra 5 ejecuciones seguidas. ¿Se repite alguna cita?
  • 🟡 Reto 2: envía una cita diferente según el día: lunes motivación, miércoles humor, viernes reflexión (tres archivos).
  • 🟠 Reto 3: guarda en un archivo las citas ya enviadas y evita repetirlas hasta haberlas usado todas.
  • 🔴 Reto 4: separa la lógica (¿toca enviar hoy?, ¿qué cita?) del envío en funciones distintas, de modo que puedas probar la lógica sin conexión, pasándole una fecha cualquiera: debe_enviar(fecha).

4. 🚀 Proyecto del día: el Felicitador Automático de Cumpleaños

(Fuentes: 248 - Automated Birthday Wisher Project Challenge.md, 249 - Solution & Walkthrough for the Automated Birthday Wisher.md)

Problema

Enviar automáticamente un correo de "¡Feliz cumpleaños!" a las personas de una lista, el día de su cumpleaños, con una carta elegida al azar y personalizada con su nombre.

Contexto

La profesora cuenta que su mamá espera la felicitación a las 8:00 a. m. en punto. El programa revisa un CSV de cumpleaños, elige una de tres cartas (para que no llegue la misma todos los años), cambia el marcador del nombre y envía el correo.

Materiales

Archivo Contenido
birthdays.csv Columnas name,email,year,month,day
letter_templates/letter_1.txt a letter_3.txt Cartas con un marcador de posición del nombre (por ejemplo, [NAME])

Tres niveles de dificultad (lección 248): el curso ofrece tres proyectos iniciales. Normal, con muchas pistas; difícil, con pocas pistas; y muy difícil, que solo dice qué debe hacer el programa. Elige el tuyo y anótalo.

Mensaje del curso sobre la solución (249): "No existe una verdadera respuesta correcta". Si tu programa funciona con otro método, está bien.

Requisitos funcionales

  1. Leer birthdays.csv.
  2. Si la fecha de hoy (mes y día) coincide con un cumpleaños, elegir una carta al azar.
  3. Reemplazar el marcador por el nombre de la persona.
  4. Enviar la carta al correo de esa persona con el asunto "Happy Birthday!".

Construcción paso a paso (solución del curso, nivel normal)

Paso 1 — Datos de prueba. Reemplaza los datos de birthdays.csv por datos inventados y pon en una fila el mes y día de hoy, con un correo al que tengas acceso (o del servidor local). El año no importa.

Paso 2 — ¿Hoy cumple alguien? Es el paso más difícil.

🔍 Antes de programar

  • ¿Qué dos datos de la fecha de hoy necesitas? ¿Por qué no el año?
  • Si guardas los cumpleaños en un diccionario, ¿qué usarías como clave para buscar rápido con in?

Solución del curso:

Python
from datetime import datetime
import pandas

today = datetime.now()
today_tuple = (today.month, today.day)

data = pandas.read_csv("birthdays.csv")
birthdays_dict = {(data_row["month"], data_row["day"]): data_row
                  for (index, data_row) in data.iterrows()}

if today_tuple in birthdays_dict:
    ...
  • La clave es una tupla (mes, día) y el valor es la fila completa de datos.
  • data_row["month"] y data_row.month son equivalentes.
  • today_tuple in birthdays_dict compara con las claves (verificado: la tupla de enteros de Python coincide con los números que lee pandas).

Paso 3 — Elegir y personalizar la carta:

Python
    birthday_person = birthdays_dict[today_tuple]
    file_path = f"letter_templates/letter_{random.randint(1, 3)}.txt"
    with open(file_path) as letter_file:
        contents = letter_file.read()
        contents = contents.replace("[NAME]", birthday_person["name"])

Paso 4 — Enviar: igual que en la sección 1, con to_addrs=birthday_person["email"] y el mensaje f"Subject:Happy Birthday!\n\n{contents}".

📖 El error que el curso muestra a propósito

En la primera ejecución, el correo llegó con el marcador sin reemplazar. El código decía:

Python
contents.replace("[NAME]", birthday_person["name"])

Las cadenas son inmutables: replace no cambia la cadena original, sino que devuelve una nueva. Hay que guardar el resultado (verificado):

Python
contents = contents.replace("[NAME]", birthday_person["name"])

Pruebas

Caso Resultado esperado
Nadie cumple hoy No se envía nada, sin errores
Una persona cumple hoy Llega una carta con su nombre
Ejecutar tres veces Cambia la carta (al azar)
Nombre con tilde o ñ (por ejemplo, "Mamá" o "Íñigo") ⚠️ (ver 🧪)
Dos personas cumplen hoy ⚠️ (ver 🧪)
Una persona nacida el 29 de febrero, en un año no bisiesto ⚠️ (ver 🧪)

🧪 Prueba y depuración

  1. Dos cumpleaños el mismo día. Pon a dos personas con la misma fecha. El diccionario solo guarda una por clave: la última reemplaza a la primera (verificado: con Ana y Luis el 14 de julio, solo queda Luis). ¡Ana no recibe felicitación! Corrige el programa para que funcione con varias personas. (Pista: recorre las filas con iterrows y compara cada una, o guarda como valor una lista de filas.)
  2. Tildes y ñ. Un nombre como "Mamá" en el mensaje produce UnicodeEncodeError con sendmail (verificado). Cámbialo a EmailMessage y send_message.
  3. 29 de febrero. En un año no bisiesto, (2, 29) nunca coincide con hoy. Decide si esa persona debe recibir el correo el 28 de febrero o el 1 de marzo, e impleméntalo.
  4. Doble envío. Si el programa se ejecuta dos veces el mismo día, la persona recibe dos correos. ¿Cómo lo evitarías? (Pista: guardar la fecha del último envío.)

Reflexión ética

La profesora dice: "La ética de todo este proyecto realmente depende de ti". Ella misma no lo usa con su mamá. Escribe un párrafo: ¿cuándo un mensaje automático es útil y cuándo es falso? ¿Le avisarías a la persona que el saludo es automático?

⭐ Extensión del proyecto

  • Cartas en español y en inglés según una columna idioma.
  • Un correo para ti con los cumpleaños de la próxima semana.
  • Guardar un registro (log.txt) de cada correo enviado, con fecha y hora.

5. Ejecutar el programa todos los días en la nube

(Fuentes: 250-schedule-your-birthday-wisher-as-a-github-action.md, 251 - Optional (NOT FREE any more)- Run Your Python Code in the Cloud!.md)

🎯 Objetivo de aprendizaje

Entender cómo se programa la ejecución automática de un script y cómo se protegen las contraseñas con variables de entorno.

📖 El problema

El felicitador solo funciona si alguien lo ejecuta cada día. Abrir el editor y pulsar Ejecutar a diario es más trabajo que felicitar a mano. La solución es un servicio que ejecute el programa en la nube a una hora fija.

📝 Contenido original del curso: GitHub Actions (lección 250)

El curso propone GitHub Actions, gratuito para este uso:

  1. Crear una cuenta en GitHub.
  2. Hacer un fork (copia propia) del repositorio plantilla appbrewery/scheduled-tasks.
  3. Habilitar los workflows en la pestaña Actions. El curso advierte: revisa siempre los archivos de un workflow antes de habilitarlo; nunca ejecutes código de internet sin mirarlo.
  4. Copiar el código en main.py y los archivos que necesite (.csv, .txt).
  5. Reemplazar las contraseñas escritas en el código por variables de entorno:
Python
import os

# ANTES (¡no lo hagas!: cualquiera que vea el código ve la contraseña)
MY_EMAIL = "myemail@gmail.com"
MY_PASSWORD = "mysecretpassword"

# DESPUÉS (los secretos se guardan en GitHub)
MY_EMAIL = os.environ.get("MY_EMAIL")
MY_PASSWORD = os.environ.get("MY_PASSWORD")
  1. Listar en requirements.txt las bibliotecas que no vienen con Python (por ejemplo, pandas).
  2. Guardar los secretos en Settings → Secrets and variables → Actions → New repository secret.
  3. Programar la hora en .github/workflows/scheduled.yml con una expresión cron.
  4. Probar con el workflow "Test Setup" y luego ejecutar "Daily Script" manualmente.

📖 La expresión cron y la hora UTC

Texto
cron: '0 9 * * *'
       │ │
       │ └─ hora (0-23) en UTC
       └─── minuto (0-59)

Los servidores usan la hora UTC (tiempo universal). La tabla del curso no incluye Colombia, así que la calculamos (adaptación): Colombia está en UTC−5 todo el año (no hay horario de verano). Para saber la hora UTC, suma 5 a la hora de Colombia.

Quieres que se ejecute en Colombia a las… Hora UTC Expresión cron
6:00 a. m. 11 '0 11 * * *'
8:00 a. m. 13 '0 13 * * *'
9:00 a. m. 14 '0 14 * * *'

🛠️ Nota técnica (análisis de esta adaptación): en el servidor de GitHub, datetime.now() también devuelve la hora UTC. Si programas la ejecución a las 3:00 UTC, en Colombia son las 10:00 p. m. del día anterior… pero el programa creerá que ya es el día siguiente y felicitará a la persona un día antes. Con horas entre las 5:00 y las 23:59 UTC (de las 00:00 a las 18:59 en Colombia) la fecha coincide.

Problemas frecuentes que señala el curso:

  • La ejecución puede retrasarse de 5 a 15 minutos: es normal.
  • Si el repositorio no tiene actividad durante 60 días, GitHub pausa la programación. Un cambio pequeño la reactiva.
  • No module named 'pandas': falta en requirements.txt.
  • Los nombres de los secretos distinguen mayúsculas: MY_EMAIL no es my_email.
  • Gmail exige la contraseña de aplicación y, normalmente, el puerto 587.
  • Consejo del curso: provoca a propósito un error para ver cómo se muestra, porque no hay consola.

📝 PythonAnywhere (lección 251, opcional)

El curso mostraba antes PythonAnywhere: subir los archivos, probar con python3 main.py en una consola y crear una tarea programada (también en UTC). La lección misma aclara que la programación de tareas ya no es gratuita. El video incluye además un paso para "desbloquear" el acceso de Gmail que ya no existe; hoy se usa la contraseña de aplicación.

🏫 Para el aula: crear cuentas en GitHub tiene una edad mínima según sus condiciones de uso, y estos pasos implican guardar contraseñas reales. Recomendación: el docente hace la demostración con una cuenta institucional de prueba, y los estudiantes practican cron y variables de entorno en su computador.

⚠️ Errores frecuentes

Error Qué ocurre Solución
os.environ.get("MY_EMAIL") devuelve None La variable no existe (verificado); después falla el login Revisa el nombre exacto del secreto; valida con un if antes de conectarte
Programar a las 8 en UTC pensando en la hora de Colombia El correo llega a las 3:00 a. m. Suma 5 horas
Subir el código con la contraseña escrita Cualquiera la puede ver, incluso en el historial Usa secretos; si ya la subiste, cámbiala de inmediato

Retos adicionales (creados para esta adaptación)

  • 🟢 Reto 1: escribe las expresiones cron para: todos los días a las 7:30 a. m. de Colombia, y todos los lunes a las 6:00 a. m. de Colombia (investiga el quinto campo).
  • 🟡 Reto 2: en tu computador, crea una variable de entorno de prueba y léela con os.environ.get. Haz que el programa muestre un mensaje claro si no existe.
  • 🟠 Reto 3: haz que el felicitador calcule la fecha en la hora de Colombia aunque el servidor esté en UTC. (Investiga zoneinfo y ZoneInfo("America/Bogota").)
  • 🔴 Reto 4: en tu sistema operativo existe una forma de programar tareas sin la nube: el Programador de tareas de Windows o cron en Linux y Mac. Investiga cuál tiene tu equipo, compara sus ventajas y desventajas con GitHub Actions y preséntalas en una tabla.

6. 🌟 Proyecto extra: el Boletín Diario de Tareas del Curso

Situación

En el grupo de décimo, cada semana se olvidan entregas. El representante del curso propone un programa que, cada mañana, envíe un boletín con las tareas que se entregan hoy y mañana.

Requisitos

  1. Un tareas.csv con las columnas materia,descripcion,fecha_entrega (formato AAAA-MM-DD) y al menos 8 tareas inventadas.
  2. Leer el CSV con pandas y convertir cada fecha de texto a fecha (investiga datetime.strptime o date.fromisoformat).
  3. Seleccionar las tareas de hoy y las de mañana (investiga timedelta(days=1)).
  4. Construir un correo con dos secciones: "Para hoy" y "Para mañana", con tildes correctas (EmailMessage).
  5. Si no hay tareas, enviar un mensaje corto: "¡Hoy no hay entregas!".
  6. Si es sábado o domingo, no enviar nada.
  7. Probar todo con el servidor local.

Decisiones que debes tomar

  • ¿Envías el boletín a una lista de correos o a uno solo? ¿Dónde guardas esa lista?
  • Si el viernes hay tareas para el lunes, ¿deberían aparecer como "para mañana"?

Casos de prueba

Caso Esperado
Hoy hay 2 tareas y mañana 1 Boletín con 2 y 1 tareas en su sección
No hay tareas Mensaje "¡Hoy no hay entregas!"
Es domingo No se envía nada
Una fecha mal escrita en el CSV (2026-13-40) El programa avisa qué fila está mal y continúa

7. 🏆 Proyecto integrador de los Días 24 a 32: el Asistente del Director de Grupo

Proyecto integrador creado para esta adaptación. Reúne archivos y rutas (Día 24), CSV y pandas (Día 25), comprensión de listas y diccionarios (Día 26), excepciones y JSON (Día 30), y correo y fechas (Día 32).

Situación problema

El director de grupo de un curso de décimo dedica mucho tiempo a tareas repetitivas: recordar cumpleaños, enviar recordatorios de entregas y preparar un resumen semanal de asistencia para coordinación.

Pregunta orientadora

¿Cómo automatizar tareas repetitivas con datos y correos, cuidando los datos personales de los estudiantes?

Objetivo

Construir un programa de consola, Asistente del Director de Grupo, que cada día decide qué correos enviar y los envía (al servidor local durante el desarrollo).

Requerimientos

  1. Datos ficticios: estudiantes.csv (nombre, correo, fecha de nacimiento), tareas.csv y asistencia.csv (fecha, nombre, presente: sí/no).
  2. Cumpleaños: felicitación a cada estudiante que cumpla hoy (varias personas el mismo día deben funcionar).
  3. Recordatorios: boletín de tareas de hoy y mañana (como en la sección 6).
  4. Resumen semanal: los viernes, un correo al director con el porcentaje de asistencia de cada estudiante, calculado con pandas.
  5. Registro: un registro.json con cada correo enviado (fecha, tipo, destinatario) para no enviar dos veces lo mismo el mismo día.
  6. Configuración: servidor, puerto y remitente en un config.json; la contraseña solo en una variable de entorno.
  7. Robustez: un archivo faltante o una fila mal escrita no detiene el programa; se informa en consola.
  8. Todas las funciones que deciden "qué enviar" reciben la fecha como parámetro, para poder probarlas con cualquier día.

Restricciones

  • Correos con EmailMessage (tildes y ñ correctas).
  • Prohibido escribir contraseñas en el código.
  • Solo datos inventados. Incluye en el documento un párrafo sobre la Ley 1581 de 2012: ¿qué autorizaciones necesitaría el colegio para usar datos reales?

Etapas de desarrollo

Etapa Entregable parcial
1. Diseño Lista de tareas automáticas, archivos y diagrama de flujo diario
2. Lógica sin correo Funciones que, dada una fecha, devuelven qué correos enviar (probadas con 5 fechas distintas)
3. Envío Correos llegando al servidor local
4. Registro y robustez registro.json, manejo de errores
5. Programación Expresión cron propuesta (hora de Colombia → UTC)
6. Presentación Demostración con fechas simuladas

Entregables

  • Código organizado en funciones (y, si quieres, en varios archivos).
  • Datos ficticios y config.json (sin contraseñas).
  • Tabla de 10 casos de prueba con resultados.
  • Registro de errores con al menos 5 errores reales.
  • Reflexión sobre datos personales y ética de la automatización.

Casos de prueba mínimos

Caso Esperado
Fecha simulada con 2 cumpleaños 2 felicitaciones
Ejecutar dos veces el mismo día La segunda vez no se envía nada nuevo
Viernes Además, el resumen de asistencia
Falta asistencia.csv Aviso en consola; los demás correos sí se envían
Variable de entorno de la contraseña sin definir (modo real) Mensaje claro; no intenta conectarse

Criterios técnicos de funcionamiento

  • La lógica se puede probar sin conexión.
  • No hay código repetido para enviar correos: una sola función de envío.
  • Cambiar de servidor local a servidor real solo requiere cambiar config.json y la variable de entorno.

Posibles extensiones

Adjuntar el resumen de asistencia como archivo CSV (investiga add_attachment), o convertirlo en una aplicación Tkinter (Días 27 a 31).


8. 🧩 Comprueba lo aprendido

Conceptuales

  1. ¿Qué es SMTP? Explícalo con la analogía de la oficina de correos.
  2. ¿Para qué sirve starttls()? ¿Qué podría pasar sin él?
  3. ¿Por qué se usa una contraseña de aplicación en lugar de la contraseña normal?
  4. ¿Por qué el felicitador usa (mes, día) como clave y no la fecha completa con el año?
  5. ¿Qué es una variable de entorno y por qué protege mejor una contraseña?

Análisis de código

Python
birthdays_dict = {(row["month"], row["day"]): row for (index, row) in data.iterrows()}

Explica cada parte: ¿qué es la clave?, ¿qué es el valor?, ¿de dónde sale row?

Predicción

  1. Hoy es miércoles. ¿Qué devuelve dt.datetime.now().weekday()?
  2. ¿Qué imprime este código?
Python
texto = "Querido [NAME],"
texto.replace("[NAME]", "Ana")
print(texto)
  1. Un workflow tiene cron: '0 12 * * *'. ¿A qué hora se ejecuta en Colombia?

Depuración

Python
connection.sendmail(
    from_addr=MY_EMAIL,
    to_addrs=persona["email"],
    msg=f"Subject:¡Feliz cumpleaños!\n\n{carta}"
)

El programa falla con UnicodeEncodeError. ¿Por qué? Reescríbelo para que funcione.

Python
if weekday == 1:
    # enviar la cita del lunes

El estudiante asegura que el lunes no recibió nada. ¿Cuál es el error?

Modificación

Cambia el felicitador para que, además de a la persona, te envíe a ti una copia con el asunto "Hoy cumple [nombre]".

Creación

Escribe dias_para_cumpleanos(fecha_nacimiento, hoy) que devuelva cuántos días faltan para el próximo cumpleaños. Pruébala con al menos 4 fechas, incluido un cumpleaños que ya pasó este año.


9. 🤖 Programar con apoyo de IA

Actividad 1: verificar información técnica que cambia con el tiempo. Este día tiene mucha información que se desactualiza: direcciones SMTP, opciones de seguridad de Gmail, servicios gratuitos. Pregunta a una IA: "¿Cuál es el servidor SMTP de Outlook.com y cómo me autentico desde Python?".

  1. Compara su respuesta con la lección 244 del curso.
  2. Compárala con la página de ayuda oficial de Microsoft.
  3. ¿Coinciden las tres fuentes? ¿Cuál es la más reciente? ¿La IA dijo en algún momento que su información podía estar desactualizada?

Actividad 2: nunca compartas secretos. Si le pides ayuda a una IA con tu código, borra primero correos reales y contraseñas. Revisa el código que pegues y reemplázalos por "cuenta_de_prueba@..." y "CONTRASEÑA". Explica por qué es importante, aunque la IA "no se lo diga a nadie".

Actividad 3: ¿la IA prevé los casos difíciles? Pide a una IA que escriba el felicitador de cumpleaños. Comprueba si su solución funciona con: dos cumpleaños el mismo día, un nombre con tilde y el 29 de febrero. Registra qué casos falló y corrígelos tú.


10. 🛠️ Correcciones técnicas al material original

Archivo Afirmación o comportamiento del curso Observación Tratamiento
244 Hotmail: smtp.live.com; Outlook: outlook.office365.com Datos desactualizados o incorrectos para el envío; la autenticación de Microsoft cambió Nota técnica de la sección 1
245 Hacer las cuentas "menos seguras" Google eliminó esa opción; se usa contraseña de aplicación con verificación en 2 pasos Nota técnica de la sección 1
245, 247, 249 sendmail con el mensaje como texto Solo acepta ASCII: con ñ, tildes o comillas tipográficas lanza UnicodeEncodeError (verificado). Alternativa: EmailMessage + send_message Nota técnica de la sección 1; 🧪 del proyecto
249 Diccionario de cumpleaños con clave (mes, día) Si dos personas cumplen el mismo día, solo se felicita a una (verificado) 🧪 del proyecto
249 Comparación de (mes, día) Las personas nacidas el 29 de febrero no reciben correo en años no bisiestos 🧪 del proyecto
250 datetime.now() en GitHub Actions En el servidor es hora UTC; con ciertas horas, la fecha no coincide con la de Colombia Nota técnica de la sección 5
250 Tabla de zonas horarias No incluye Colombia: UTC−5 sin horario de verano Sección 5
251 PythonAnywhere gratuito; desbloqueo de Gmail La propia lección dice que ya no es gratuito; el desbloqueo ya no existe Sección 5

11. 📋 Matriz de trazabilidad

Contenido original Archivo de origen Concepto desarrollado Ejercicio original Reto adicional Proyecto relacionado
Objetivos: smtplib, datetime, felicitador 243 Sección 0 — — Sección 4
Direcciones SMTP, contraseña de aplicación, puerto 587 244 Sección 1 — Reto 4 de la sección 1 Secciones 4, 5 y 7
SMTP, starttls, login, sendmail, asunto, with 245 Sección 1 Enviar un correo de prueba Retos 1–4 de la sección 1; servidor local Secciones 3, 4, 6 y 7
datetime.now, atributos, weekday, crear fechas 246 Sección 2 Fecha de nacimiento Retos 1–4 de la sección 2 Secciones 3, 4, 6 y 7
Citas motivacionales los lunes 247 Sección 3 Desafío 1 Retos 1–4 de la sección 3 Sección 6
Felicitador: niveles de dificultad, demostración, ética 248 Sección 4 Proyecto del curso Reflexión ética Sección 7
Solución: tupla de hoy, comprensión con iterrows, replace, envío 249 Sección 4 Proyecto del curso 🧪 del proyecto Sección 7
GitHub Actions, secretos, os.environ, cron, UTC 250 Sección 5 Programar el script Retos 1–4 de la sección 5 Sección 7
PythonAnywhere (ya no gratuito) 251 Sección 5 — Reto 4 de la sección 5 —
(No existe en el curso original) — — — Boletín Diario de Tareas Sección 6
(No existe en el curso original) — — — Asistente del Director de Grupo (Días 24–32) Sección 7

12. 📦 Evidencias de aprendizaje del Día 32

  • Captura de la consola del servidor local mostrando un correo recibido con tildes.
  • El programa de citas de los lunes con tu propio quotes.txt.
  • El felicitador funcionando con datos inventados, con las cuatro correcciones de la sección 🧪.
  • Tu tabla de expresiones cron para la hora de Colombia.
  • La reflexión ética sobre los saludos automáticos y el párrafo sobre la Ley 1581 de 2012.
  • El Boletín Diario de Tareas (proyecto extra) o el Asistente del Director de Grupo (proyecto integrador).
  • Cuaderno de errores: SMTPAuthenticationError, SMTPServerDisconnected, UnicodeEncodeError con ñ, starttls en el servidor local, weekday sin paréntesis, replace sin asignar, cumpleaños duplicados, 29 de febrero, variable de entorno en None y confusión de UTC.