Skip to content
Empieza con 100 créditos de verificación gratis
Qualisend
Todos los artículos
Ingeniería / 17 de junio de 2026

Cómo validar una dirección de correo electrónico en Python

5 minutes read

Qualisend team
Una ventana de código Python validando un correo a través de las capas de sintaxis, DNS y SMTP

Validar una dirección de correo electrónico en Python significa tres comprobaciones distintas, y la que muestran la mayoría de los tutoriales —una expresión regular— es la menos útil de todas. La validación de verdad es por capas: sintaxis, luego una consulta DNS y después una prueba de buzón por SMTP. La biblioteca estándar de Python y una pequeña dependencia cubren las dos primeras; la tercera es un problema de red que conviene delegar. Esta guía construye cada capa con código funcional.

La respuesta rápida#

Usa una expresión regular permisiva (o email.utils.parseaddr) para la sintaxis, dnspython para la consulta MX y una API de verificación para la comprobación de buzón por SMTP: primero lo más barato, cortocircuitando en cuanto una de ellas sea decisiva. No recurras a smtplib para sondear buzones desde tu aplicación: el puerto 25 de salida está bloqueado en la mayoría de los hosts, y el resultado depende de la reputación de la IP de envío y del greylisting que no querrás reimplementar.

Capa 1: sintaxis#

Mantenla permisiva: detecta erratas, no reimplementes el RFC 5322 (que no ayudaría de todos modos):

import re

SYNTAX = re.compile(r'^[^\s@"]+(?:\.[^\s@"]+)*@[^\s@.]+(?:\.[^\s@.]+)+$')

def is_valid_syntax(email: str) -> bool:
    return isinstance(email, str) and len(email) <= 320 and SYNTAX.match(email) is not None
is_valid_syntax("jane@example.com")  # True
is_valid_syntax("not-an-email")      # False
is_valid_syntax("a@@b.com")          # False

El email.utils.parseaddr de la biblioteca estándar analiza una dirección, pero es indulgente por diseño —parseaddr("nonsense") devuelve ('', 'nonsense') sin lanzar ninguna excepción—, así que es un analizador, no un validador. Una expresión regular corta es más clara para una puerta de sintaxis de sí/no.

Capa 2: ¿puede el dominio recibir correo?#

Un dominio sin registros MX no puede aceptar correo para nadie. dnspython los resuelve en un par de líneas:

import dns.resolver  # pip install dnspython

def has_mail_route(domain: str) -> bool:
    try:
        answers = dns.resolver.resolve(domain, "MX")
        return len(answers) > 0
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
        return False
has_mail_route("gmail.com")              # True
has_mail_route("company-that-folded.com")  # False

Esta única consulta elimina dominios muertos y TLD inventados sin coste alguno. Si quieres tener en cuenta los dominios que aceptan correo en un registro A sin MX, recurre a resolver A/AAAA cuando el conjunto MX esté vacío, pero una comprobación MX cubre la inmensa mayoría de las direcciones reales.

Capa 3: ¿existe realmente el buzón?#

Las capas 1 y 2 solo pueden descartar una dirección. Confirmar un buzón implica la conversación de entrega SMTP y, aunque smtplib puede abrir una, ejecutarla desde tu aplicación es una mala idea: el puerto 25 está bloqueado de forma generalizada, la respuesta depende de la reputación de la IP desde la que conectas, y los servidores receptores aplican greylisting y limitan la tasa a los desconocidos. Delega esta capa.

El POST /verify de Qualisend ejecuta el proceso completo. Las comprobaciones locales se devuelven de inmediato con la prueba SMTP en cola:

import os
import requests  # pip install requests

BASE = "https://app.qualisend.com/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"}

def verify(email: str) -> dict:
    res = requests.post(f"{BASE}/verify", headers=HEADERS, json={"email": email}, timeout=10)
    res.raise_for_status()
    return res.json()

Para la validación de registro en tiempo real, el result inmediato suele bastar para actuar. Cuando necesites el veredicto confirmado por SMTP, sondea el trabajo hasta que la prueba termine:

import time

def verify_and_wait(email: str, tries: int = 10, delay: float = 1.5):
    job_id = verify(email)["job_id"]
    for _ in range(tries):
        res = requests.get(f"{BASE}/jobs/{job_id}",
                           params={"include": "results"}, headers=HEADERS, timeout=10)
        job = res.json()
        if job["status"] == "completed":
            return job["results"][0]  # {"status": ..., "score": ..., "reason": ...}
        time.sleep(delay)
    return None  # still processing — treat as unknown, retry later

Verificar una lista por lotes#

No recorras verify() sobre un CSV dirección a dirección: eso es una ida y vuelta HTTP y un trabajo por fila. Envía la lista completa a /verify en una sola petición, recibe un único job_id y sondea una vez para todo el lote. El array results vuelve en el orden de envío:

def verify_bulk(emails: list[str], tries: int = 60, delay: float = 2.0):
    res = requests.post(f"{BASE}/verify", headers=HEADERS,
                        json={"emails": emails}, timeout=30)
    res.raise_for_status()
    job_id = res.json()["job_id"]

    for _ in range(tries):
        job = requests.get(f"{BASE}/jobs/{job_id}",
                          params={"include": "results"}, headers=HEADERS,
                          timeout=30).json()
        if job["status"] == "completed":
            return dict(zip(emails, job["results"]))
        time.sleep(delay)  # back off; large lists take longer than a single probe
    return None

Un lote de miles puede tardar minutos porque cada prueba SMTP es una conversación de red aparte, así que amplía tries/delay para las listas más grandes en lugar de martillear el endpoint de sondeo. Ejecuta primero las dos capas locales para descartar la basura evidente antes de gastar créditos: filtra con is_valid_syntax y has_mail_route, y envía solo las supervivientes.

Notas sobre async y pydantic#

requests es bloqueante, así que en una aplicación asyncio envuelve la llamada con asyncio.to_thread(verify, email) o cámbiate a httpx.AsyncClient para lanzar las llamadas de verificación de forma concurrente; dnspython incluye un resolutor asíncrono (dns.asyncresolver) por la misma razón. Si ya usas pydantic, su tipo EmailStr encaja de forma limpia como Capa 1: valida la sintaxis cuando se construye un modelo, pero se queda ahí; nunca toca el DNS ni el buzón, así que mantén la comprobación MX y la API de verificación por detrás de él.

Uniendo las capas#

Primero lo más barato, y para en cuanto tengas una respuesta:

def validate_email(email: str) -> dict:
    if not is_valid_syntax(email):
        return {"status": "undeliverable", "reason": "invalid_email"}
    domain = email.rsplit("@", 1)[1]
    if not has_mail_route(domain):
        return {"status": "undeliverable", "reason": "invalid_domain"}
    return verify(email)["result"]  # deliverable | risky | undeliverable | unknown

Las dos capas locales no cuestan nada y atrapan casi toda la basura al instante; la API se ejecuta solo sobre las direcciones que merecen la ida y vuelta. Consulta cómo funciona la verificación de correo para entender por qué las etapas se ordenan así.

Preguntas frecuentes#

¿Puedo verificar un correo en Python solo con la biblioteca estándar?#

Hasta cierto punto. re se encarga de la sintaxis y, con socket, puedes intentar una consulta DNS, pero no hay un resolutor MX integrado (de ahí dnspython), y confirmar un buzón requiere una prueba SMTP que no deberías ejecutar desde tu aplicación. La biblioteca estándar te da la sintaxis y una comprobación aproximada del dominio; la capa del buzón necesita dnspython más un servicio de verificación.

¿Por qué no usar smtplib para comprobar si un buzón existe?#

smtplib puede abrir la conversación SMTP, pero ejecutarlo desde el servidor de tu aplicación es poco fiable y arriesgado: la mayoría de los hosts bloquean el puerto 25 de salida, la respuesta depende de la reputación de tu IP de envío y sondear con cierto volumen hace que te limiten la tasa o te incluyan en listas de bloqueo. Una API de verificación ejecuta la prueba desde una infraestructura con reputación gestionada y diseñada para ello.

¿Es dnspython la biblioteca adecuada para las consultas MX?#

Sí, es el kit de herramientas DNS estándar y bien mantenido para Python, y resuelve los registros MX de forma limpia. Instálalo con pip install dnspython y captura NXDOMAIN, NoAnswer y NoNameservers para tratar una ruta de correo ausente como no entregable.

¿EmailStr de pydantic valida por completo una dirección de correo?#

No. EmailStr (respaldado por el paquete email-validator) solo comprueba que una cadena es sintácticamente una dirección válida cuando se construye tu modelo. No confirma que el dominio tenga registros MX ni que el buzón exista, así que se corresponde exactamente con la Capa 1. Úsalo como puerta de sintaxis en tus modelos de pydantic y, después, ejecuta la consulta DNS y la API de verificación sobre las direcciones que pasen. Además normaliza la dirección (pasa el dominio a minúsculas), lo cual resulta útil antes de deduplicar una lista.


¿Listo para añadir la capa SMTP? El plan gratuito incluye 100 créditos que ejecutan el proceso completo, y la referencia de la API tiene los endpoints completos de /verify y /jobs con ejemplos listos para copiar y pegar en siete lenguajes.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started