Cómo enviar emails con Python: guía práctica con ejemplos

D
DanisCh
(Actualizado: ) • 12 min de lectura
Cómo enviar emails con Python: guía práctica con ejemplos
Python

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 destinatario

Los 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

  1. Ve a tu cuenta de Google → Seguridad
  2. En "¿Cómo inicias sesión en Google?" activa la Verificación en dos pasos (obligatorio para App Passwords)
  3. Vuelve a Seguridad → busca "Contraseñas de aplicaciones"
  4. Selecciona aplicación: "Correo" → dispositivo: "Otro (nombre personalizado)" → escribe "Python Script"
  5. Haz clic en Generar
  6. 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 mnop
import 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 SendGrid

smtplib vs SendGrid — cuándo usar cada uno

Criteriosmtplib + GmailSendGrid / Mailgun
Límite de envío500/día (Gmail gratuito)100/día gratis, ilimitado de pago
Instalación✅ Incluido en Pythonpip install sendgrid
Configuración✅ Solo App PasswordDominio verificado + API key
Tasa de entregaBuena para pocas✅ Excelente a escala
Analytics❌ No tiene✅ Aperturas, clics, rebotes
PlantillasManual✅ Editor visual incluido
Ideal paraScripts, notificaciones, proyectos personalesApps 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-8

Resumen: lo que aprendiste hoy

  • ✅ Python tiene smtplib incluido 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
  • ✅ EmailMessage es 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. 🚀

¿Te ha gustado esta entrada?

Compártela con tus compañeros para que también sigan aprendiendo.

Comunidad y Comentarios

0 COMENTARIOS

No hay comentarios todavía. Sé el primero en compartir tu opinión.

Escribe tu opinión
Respondiendo a