Enviar un email desde Python es una de esas capacidades que parece avanzada pero que en realidad dominas en 10 minutos. Y una vez que sabes hacerlo, abre un abanico enorme de posibilidades: notificaciones automáticas cuando un script termina, alertas cuando algo falla, informes diarios por correo, confirmaciones de registro en tu app.
Python tiene todo lo necesario incluido en su biblioteca estándar con el módulo smtplib. En este artículo vas a aprender a enviar emails de texto plano, emails HTML con diseño, emails con archivos adjuntos, emails a múltiples destinatarios y cómo escalar a producción con SendGrid cuando necesitas enviar miles de correos.
Antes de empezar, asegúrate de tener un entorno virtual Python configurado para tu proyecto y de aplicar buenas prácticas de manejo de errores en el código.
Cómo funciona el envío de emails por SMTP
SMTP (Simple Mail Transfer Protocol) es el protocolo estándar para enviar correos electrónicos. Cuando envías un email con Python, tu script actúa como un cliente de correo que se conecta a un servidor SMTP (Gmail, Outlook, tu propio servidor) y le entrega el mensaje para que lo envíe.
Tu script Python
↓
smtplib.SMTP("smtp.gmail.com", 587) ← conexión al servidor SMTP
↓
server.starttls() ← cifrado TLS
↓
server.login(usuario, contraseña) ← autenticación
↓
server.send_message(msg) ← entrega el mensaje
↓
Servidor SMTP de Gmail
↓
Servidor SMTP del destinatario
↓
Bandeja de entrada del destinatarioLos dos modos de conexión SMTP
# Modo 1: STARTTLS — puerto 587 (recomendado)
# Empieza con conexión no cifrada y luego sube a TLS
smtplib.SMTP("smtp.gmail.com", 587)
server.starttls()
# Modo 2: SSL/TLS directo — puerto 465
# Cifrado desde el primer momento
smtplib.SMTP_SSL("smtp.gmail.com", 465)Configurar Gmail para enviar desde Python
Gmail no permite usar tu contraseña habitual para conectarte desde scripts externos por seguridad. Necesitas una Contraseña de aplicación (App Password) — una contraseña de 16 caracteres generada específicamente para este uso.
Cómo generar una App Password en Gmail
- Ve a tu cuenta de Google → Seguridad
- En "¿Cómo inicias sesión en Google?" activa la Verificación en dos pasos (obligatorio para App Passwords)
- Vuelve a Seguridad → busca "Contraseñas de aplicaciones"
- Selecciona aplicación: "Correo" → dispositivo: "Otro (nombre personalizado)" → escribe "Python Script"
- Haz clic en Generar
- Copia la contraseña de 16 caracteres que aparece — solo se muestra una vez
Guardar las credenciales de forma segura
Nunca escribas las credenciales directamente en el código. Si subes el archivo a GitHub quedarán expuestas públicamente. Usa siempre variables de entorno:
# .env (añadir a .gitignore — NUNCA subir a Git)
EMAIL_REMITENTE=tu@gmail.com
EMAIL_APP_PASSWORD=abcd efgh ijkl mnopimport os
from dotenv import load_dotenv # pip install python-dotenv
load_dotenv() # carga las variables del archivo .env
EMAIL_REMITENTE = os.getenv("EMAIL_REMITENTE")
EMAIL_APP_PASSWORD = os.getenv("EMAIL_APP_PASSWORD")El email más simple — texto plano
import smtplib
import os
from email.message import EmailMessage
from dotenv import load_dotenv
load_dotenv()
def enviar_email_simple(destinatario, asunto, cuerpo):
"""Envía un email de texto plano."""
# Crear el mensaje
msg = EmailMessage()
msg["From"] = os.getenv("EMAIL_REMITENTE")
msg["To"] = destinatario
msg["Subject"] = asunto
msg.set_content(cuerpo)
# Enviar
try:
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.ehlo() # identificarse con el servidor
server.starttls() # activar cifrado TLS
server.ehlo() # re-identificarse tras TLS
server.login(
os.getenv("EMAIL_REMITENTE"),
os.getenv("EMAIL_APP_PASSWORD")
)
server.send_message(msg)
print(f"✅ Email enviado a {destinatario}")
return True
except smtplib.SMTPAuthenticationError:
print("❌ Error de autenticación — verifica el usuario y App Password")
return False
except smtplib.SMTPException as e:
print(f"❌ Error SMTP: {e}")
return False
except Exception as e:
print(f"❌ Error inesperado: {e}")
return False
# Uso
enviar_email_simple(
destinatario = "destinatario@ejemplo.com",
asunto = "Prueba desde Python",
cuerpo = "Hola, este es un email enviado automáticamente con Python."
)Email con contenido HTML
La mayoría de los emails modernos usan HTML para el diseño. Python permite enviar emails con ambas versiones — texto plano (para clientes que no soportan HTML) y HTML (para los que sí).
import smtplib
import os
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from dotenv import load_dotenv
load_dotenv()
def enviar_email_html(destinatario, asunto, texto_plano, html):
"""Envía un email con versión texto y versión HTML."""
msg = MIMEMultipart("alternative")
msg["From"] = os.getenv("EMAIL_REMITENTE")
msg["To"] = destinatario
msg["Subject"] = asunto
# Adjuntar ambas versiones — el cliente elige la que soporta
# El cliente usa la ÚLTIMA versión añadida si puede mostrarla
parte_texto = MIMEText(texto_plano, "plain", "utf-8")
parte_html = MIMEText(html, "html", "utf-8")
msg.attach(parte_texto)
msg.attach(parte_html) # ← el cliente usará esta si puede
try:
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login(
os.getenv("EMAIL_REMITENTE"),
os.getenv("EMAIL_APP_PASSWORD")
)
server.send_message(msg)
print(f"✅ Email HTML enviado a {destinatario}")
return True
except Exception as e:
print(f"❌ Error: {e}")
return False
# Uso con HTML real
texto_plano = """
Hola Ana,
Tu pedido #1042 ha sido confirmado.
Productos: Laptop Pro 15 x1 — $899.99
Total: $899.99
Gracias por tu compra.
"""
html_contenido = """
✅ Pedido Confirmado
Hola Ana,
Tu pedido #1042 ha sido confirmado y está siendo procesado.
Producto
Precio
Laptop Pro 15 x1
$899.99
Total
$899.99
Ver mi pedido
Gracias por tu compra en MiTienda.com
"""
enviar_email_html(
destinatario = "ana@ejemplo.com",
asunto = "✅ Tu pedido #1042 ha sido confirmado",
texto_plano = texto_plano,
html = html_contenido
)Email con archivos adjuntos
import smtplib
import os
from email.message import EmailMessage
from pathlib import Path
from dotenv import load_dotenv
load_dotenv()
def enviar_con_adjunto(destinatario, asunto, cuerpo, rutas_adjuntos):
"""
Envía un email con uno o varios archivos adjuntos.
rutas_adjuntos: lista de rutas de archivos a adjuntar
"""
msg = EmailMessage()
msg["From"] = os.getenv("EMAIL_REMITENTE")
msg["To"] = destinatario
msg["Subject"] = asunto
msg.set_content(cuerpo)
# Adjuntar archivos
for ruta in rutas_adjuntos:
ruta = Path(ruta)
if not ruta.exists():
print(f"⚠️ Archivo no encontrado: {ruta}")
continue
# Detectar el tipo MIME según la extensión
extension = ruta.suffix.lower()
tipos_mime = {
".pdf": ("application", "pdf"),
".png": ("image", "png"),
".jpg": ("image", "jpeg"),
".jpeg": ("image", "jpeg"),
".xlsx": ("application", "vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
".csv": ("text", "csv"),
".zip": ("application", "zip"),
".docx": ("application", "vnd.openxmlformats-officedocument.wordprocessingml.document"),
}
tipo_principal, subtipo = tipos_mime.get(extension, ("application", "octet-stream"))
with open(ruta, "rb") as f:
msg.add_attachment(
f.read(),
maintype = tipo_principal,
subtype = subtipo,
filename = ruta.name
)
print(f"📎 Adjunto añadido: {ruta.name}")
try:
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login(
os.getenv("EMAIL_REMITENTE"),
os.getenv("EMAIL_APP_PASSWORD")
)
server.send_message(msg)
print(f"✅ Email con adjuntos enviado a {destinatario}")
return True
except Exception as e:
print(f"❌ Error: {e}")
return False
# Enviar informe mensual con PDF y CSV adjuntos
enviar_con_adjunto(
destinatario = "jefe@empresa.com",
asunto = "Informe de ventas — Abril 2026",
cuerpo = "Adjunto el informe de ventas de abril. Saludos.",
rutas_adjuntos = ["informes/ventas_abril.pdf", "datos/ventas_abril.csv"]
)Enviar a múltiples destinatarios
import smtplib
import os
from email.message import EmailMessage
from dotenv import load_dotenv
load_dotenv()
def enviar_a_multiples(destinatarios, cc=None, bcc=None, asunto="", cuerpo=""):
"""
Envía un email a múltiples destinatarios con CC y BCC opcionales.
destinatarios: lista de emails principales
cc: lista de emails en copia
bcc: lista de emails en copia oculta
"""
msg = EmailMessage()
msg["From"] = os.getenv("EMAIL_REMITENTE")
msg["To"] = ", ".join(destinatarios)
msg["Subject"] = asunto
if cc:
msg["Cc"] = ", ".join(cc)
# BCC: se añade a los destinatarios del servidor pero NO al encabezado
msg.set_content(cuerpo)
# Lista completa de receptores para el servidor SMTP
todos_los_receptores = destinatarios.copy()
if cc:
todos_los_receptores.extend(cc)
if bcc:
todos_los_receptores.extend(bcc) # BCC no aparece en el mensaje
try:
with smtplib.SMTP("smtp.gmail.com", 587) as server:
server.starttls()
server.login(
os.getenv("EMAIL_REMITENTE"),
os.getenv("EMAIL_APP_PASSWORD")
)
# send_message con lista de receptores explícita (incluye BCC)
server.sendmail(
from_addr = os.getenv("EMAIL_REMITENTE"),
to_addrs = todos_los_receptores,
msg = msg.as_string()
)
print(f"✅ Email enviado a {len(todos_los_receptores)} destinatarios")
return True
except Exception as e:
print(f"❌ Error: {e}")
return False
# Uso
enviar_a_multiples(
destinatarios = ["ana@empresa.com", "luis@empresa.com"],
cc = ["directora@empresa.com"],
bcc = ["archivo@empresa.com"],
asunto = "Reunión de equipo — Lunes 9:00",
cuerpo = "Recordatorio: reunión de equipo el lunes a las 9:00."
)Template de email con Jinja2 — emails dinámicos
# pip install jinja2
from jinja2 import Template
plantilla_html = """
Hola, {{ nombre }}!
Tu resumen de actividad del {{ fecha }}:
{{ item }}
{% if total_ventas > 1000 %}
🎉 ¡Excelente semana! Superaste los $1,000 en ventas.
{% else %}
Sigue así, vas bien.
{% endif %}
Saludos,
El equipo
"""
def generar_email_personalizado(datos_usuario):
"""Genera el HTML del email con datos del usuario."""
template = Template(plantilla_html)
return template.render(
nombre = datos_usuario["nombre"],
fecha = datos_usuario["fecha"],
actividad = datos_usuario["actividad"],
total_ventas = datos_usuario["total_ventas"]
)
# Enviar email personalizado a cada usuario
usuarios = [
{
"email": "ana@ejemplo.com",
"nombre": "Ana",
"fecha": "semana del 21 al 27 de abril",
"actividad": ["15 pedidos procesados", "3 clientes nuevos", "2 devoluciones"],
"total_ventas": 1250.00
},
{
"email": "luis@ejemplo.com",
"nombre": "Luis",
"fecha": "semana del 21 al 27 de abril",
"actividad": ["8 pedidos procesados", "1 cliente nuevo"],
"total_ventas": 650.00
},
]
for usuario in usuarios:
html_personalizado = generar_email_personalizado(usuario)
enviar_email_html(
destinatario = usuario["email"],
asunto = f"Tu resumen semanal, {usuario['nombre']}",
texto_plano = f"Hola {usuario['nombre']}, aquí tu resumen semanal.",
html = html_personalizado
)SendGrid — para producción y envíos masivos
smtplib con Gmail es perfecto para notificaciones internas, scripts personales y envíos de pocas decenas de emails. Pero Gmail tiene límites: 500 emails/día en cuentas gratuitas, 2000/día en Google Workspace. Para aplicaciones reales con cientos o miles de usuarios, necesitas un servicio de email transaccional como SendGrid.
# pip install sendgrid
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail, Attachment, FileContent, FileName, FileType, Disposition
from dotenv import load_dotenv
import base64
load_dotenv()
def enviar_con_sendgrid(destinatario, asunto, html_contenido, texto_plano=None):
"""Envía email usando la API de SendGrid (recomendado para producción)."""
message = Mail(
from_email = os.getenv("EMAIL_REMITENTE"), # debe estar verificado en SendGrid
to_emails = destinatario,
subject = asunto,
html_content = html_contenido,
plain_text_content = texto_plano
)
try:
sg = SendGridAPIClient(os.getenv("SENDGRID_API_KEY"))
response = sg.send(message)
print(f"✅ Email enviado — Status: {response.status_code}")
return response.status_code == 202 # 202 = Accepted
except Exception as e:
print(f"❌ Error SendGrid: {e}")
return False
# .env para SendGrid:
# SENDGRID_API_KEY=SG.xxxxxxxxxxxxxxxxxxxxxxxx
# EMAIL_REMITENTE=noreply@tudominio.com ← dominio verificado en SendGridsmtplib vs SendGrid — cuándo usar cada uno
| Criterio | smtplib + Gmail | SendGrid / Mailgun |
|---|---|---|
| Límite de envío | 500/día (Gmail gratuito) | 100/día gratis, ilimitado de pago |
| Instalación | ✅ Incluido en Python | pip install sendgrid |
| Configuración | ✅ Solo App Password | Dominio verificado + API key |
| Tasa de entrega | Buena para pocas | ✅ Excelente a escala |
| Analytics | ❌ No tiene | ✅ Aperturas, clics, rebotes |
| Plantillas | Manual | ✅ Editor visual incluido |
| Ideal para | Scripts, notificaciones, proyectos personales | Apps en producción, emails transaccionales masivos |
Errores comunes y cómo resolverlos
❌ SMTPAuthenticationError: Username and password not accepted
# Causa: estás usando tu contraseña normal de Gmail, no el App Password
# Solución: genera un App Password en tu cuenta de Google
# (Cuenta → Seguridad → Verificación en dos pasos → Contraseñas de app)
# Causa 2: el App Password tiene espacios — Gmail los muestra en grupos de 4
# pero debes usar la contraseña sin espacios:
# ❌ "abcd efgh ijkl mnop"
# ✅ "abcdefghijklmnop"❌ ConnectionRefusedError o Timeout
# El servidor no puede conectarse al puerto 587 o 465
# Puede ser el firewall de tu red o empresa
# Prueba el puerto alternativo:
smtplib.SMTP_SSL("smtp.gmail.com", 465) # en lugar de 587
# Verificar conectividad:
import socket
socket.create_connection(("smtp.gmail.com", 587), timeout=5)❌ Email llega a spam
# Causas y soluciones: # 1. El asunto contiene palabras como "GRATIS", "OFERTA" → evítalas en pruebas # 2. Usas Gmail para enviar a Gmail → normal en pruebas, mejora en producción # 3. Para producción real → usa SendGrid/Mailgun con dominio verificado + SPF/DKIM
❌ Caracteres especiales (ñ, acentos) corruptos
# Especifica siempre el encoding UTF-8
parte_texto = MIMEText(cuerpo, "plain", "utf-8") # ← siempre utf-8
parte_html = MIMEText(html, "html", "utf-8") # ← siempre utf-8Resumen: lo que aprendiste hoy
- ✅ Python tiene
smtplibincluido en la biblioteca estándar — no necesitas instalar nada para emails básicos - ✅ Gmail requiere una App Password (no tu contraseña normal) para conectarse por SMTP
- ✅ Guarda siempre las credenciales en variables de entorno con
python-dotenv— nunca en el código - ✅
smtplib.SMTP("smtp.gmail.com", 587)+starttls()es el modo de conexión más recomendado - ✅
EmailMessagees la forma moderna de construir mensajes (Python 3.6+) - ✅ Para emails HTML usa
MIMEMultipart("alternative")con versión texto y HTML - ✅ Los adjuntos se añaden con
msg.add_attachment()especificando el tipo MIME correcto - ✅ Para BCC, pasa los destinatarios directamente a
sendmail()sin añadirlos al encabezado - ✅ Jinja2 permite generar emails HTML personalizados con plantillas dinámicas
- ✅ Para producción y envíos masivos usa SendGrid o Mailgun — mejor tasa de entrega y analytics
🧪 ¿Tienes los fundamentos de Python bien sólidos?
Para aprovechar al máximo el envío de emails con Python necesitas dominar funciones, manejo de errores, trabajo con archivos y variables de entorno. Comprueba dónde estás:
👉 Test: Python Básico 👉 Test: Python Intermedio
¿Conseguiste enviar tu primer email con Python? ¿Usas smtplib para proyectos personales o prefieres SendGrid en producción? Cuéntanos en los comentarios 👇 — respondemos todos. 🚀
No hay comentarios todavía. Sé el primero en compartir tu opinión.