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

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

9 minutes read

Qualisend team
Ventana de código titulada forms.py que muestra tres capas de validación de Flask cada vez más estrictas — Email(), una consulta MX con check_deliverability y un POST a la API de verificación — que terminan en una insignia verde de entregable.

Para validar una dirección de correo electrónico en Flask ejecutas tres comprobaciones, no una — y Flask por sí mismo no incluye ninguna, porque Flask no tiene una capa de formularios propia. Lo que tiene es un ecosistema: WTForms (normalmente a través de Flask-WTF) para el manejo de formularios, y el paquete email-validator en el que WTForms se apoya. Entre ambos responden las dos primeras de las tres preguntas de la validación — ¿tiene la dirección la forma correcta y puede su dominio recibir correo? — pero nunca la tercera: ¿existe realmente el buzón? La validación de verdad tiene capas: la sintaxis, luego una consulta DNS y después un sondeo del buzón por SMTP. Esta guía construye cada capa como un validador de WTForms que puedes incorporar a un formulario de Flask-WTF o a una ruta JSON sencilla, y muestra dónde toma el relevo una API de verificación. Continúa donde lo deja la guía de Python, de modo que el razonamiento sobre DNS y SMTP que allí se explica se traslada intacto.

La respuesta rápida#

Usa el validador Email() de WTForms para la sintaxis, su opción check_deliverability=True (o dnspython) para la consulta MX, y una API de verificación para la comprobación del buzón por SMTP — encadenadas de lo más barato a lo más caro para que cada capa haga cortocircuito antes de que se ejecute la siguiente. No abras conexiones SMTP desde tu proceso de Flask para sondear buzones: el puerto 25 saliente está bloqueado en la mayoría de los hosts, y la respuesta depende de la reputación de la IP emisora y del greylisting que no querrás reimplementar. Cada capa descarta direcciones de forma más barata que la anterior; solo la API puede dar una por válida. Si este proceso es nuevo para ti, qué es la verificación de correo explica primero los términos.

Capa 1: validación de formato con el validador Email de WTForms#

WTForms se encarga de la capa uno, y no la implementa a mano. wtforms.validators.Email delega en el paquete email-validator, así que declarar un validador Email() en un campo te da gratis una comprobación de sintaxis real, derivada del RFC — no una expresión regular:

from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email

class SignupForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Email()])

El validador Email() necesita que el paquete esté instalado (pip install email-validator, o pip install wtforms[email]); sin él, WTForms lanza un error en el momento de la validación para avisarte. Por dentro llama a email_validator.validate_email, que también puedes usar por separado — fuera de un formulario, o en una capa de servicio — para obtener el mismo veredicto como valor booleano:

from email_validator import validate_email, EmailNotValidError

def is_valid_syntax(email: str) -> bool:
    try:
        validate_email(email, check_deliverability=False)
        return True
    except EmailNotValidError:
        return False

Ten claro exactamente qué te aporta esto. Con check_deliverability=False la comprobación lee solo la forma — nunca resuelve el DNS, nunca abre un socket y no tiene ni idea de si example.com existe. definitely-fake@gmail.com pasa. info@company-that-folded.com pasa. typo@gmial.com pasa. Las tres son inentregables, y ningún validador que solo lea la cadena te lo dirá jamás, por la misma razón por la que la validación de correo con regex falla: sintaxis y entregabilidad son preguntas distintas. Una es un hecho sobre la cadena de texto; la otra es un hecho sobre internet.

Capa 2: ¿acepta el dominio correo?#

Un dominio sin registros MX no puede aceptar correo para nadie, así que una sola consulta DNS elimina los dominios muertos, los nombres de empresa mal escritos y los TLD inventados. Aquí la pila de Flask hace algo que la de Django no hace de serie: email-validator puede ejecutar la consulta por ti. Activa check_deliverability y resolverá los registros MX del dominio (recurriendo a A/AAAA como alternativa) con dnspython, lanzando EmailUndeliverableError — una subclase de EmailNotValidError — cuando el dominio no puede recibir correo. En un formulario, eso es una única opción en el validador que ya tienes:

email = StringField("Email", validators=[DataRequired(), Email(check_deliverability=True)])

Ese único flag fusiona las capas uno y dos en un solo validador. Por separado, la misma llamada devuelve un objeto de dirección normalizada cuando el dominio pasa la comprobación:

from email_validator import validate_email, EmailNotValidError

def has_mail_route(email: str) -> bool:
    try:
        validate_email(email, check_deliverability=True)  # syntax + live MX lookup
        return True
    except EmailNotValidError:
        return False

Si prefieres ejecutar tú mismo la comprobación DNS — para cachear resultados, apuntar a tu propio resolver o poner un timeout ajustado — baja directamente a dnspython:

import dns.resolver  # pip install dnspython

def domain_has_mx(domain: str) -> bool:
    try:
        return len(dns.resolver.resolve(domain, "MX")) > 0
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
        return False

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

Las capas uno y dos solo pueden descartar una dirección. Un dominio puede publicar registros MX perfectos y aun así no tener ningún buzón en la dirección que tienes entre manos — noreply-9f2x@gmail.com es sintaxis válida en una ruta de correo activa, y sigue siendo un buzón que nunca se creó. Confirmar un buzón concreto implica la conversación de entrega SMTP: conectar al servidor de correo, emitir RCPT TO, leer la respuesta y desconectar antes de enviar nada.

En principio podrías programar eso desde Flask con smtplib. En la práctica no deberías ejecutarlo desde tu servidor de aplicación: la mayoría de los hosts en la nube bloquean el puerto 25 saliente, la respuesta depende de la reputación de la IP desde la que conectas, y los servidores receptores aplican greylisting y limitan la tasa de los remitentes desconocidos — así que un sondeo que funciona en una prueba local falla silenciosamente, o te acaba en una lista negra, en producción. Cómo funciona la verificación de correo recorre el proceso completo, dominios catch-all incluidos. Esta es la capa que vale la pena delegar.

El endpoint de verificación de Qualisend ejecuta todo el proceso — sintaxis, DNS y el sondeo del buzón por SMTP — desde una infraestructura con reputación gestionada y devuelve un veredicto. Llámalo con requests, leyendo tu clave desde el entorno:

import os
import requests  # pip install requests

QUALISEND_VERIFY_URL = "https://api.qualisend.com/v1/verify"

def verify(email: str) -> dict:
    res = requests.post(
        QUALISEND_VERIFY_URL,
        headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
        json={"email": email},
        timeout=10,
    )
    res.raise_for_status()
    return res.json()["result"]  # {"status", "score", "reason", "sub_flags"}

La respuesta es un sobre { "result": { ... } }. Lee result["status"]deliverable, risky, undeliverable o unknown — junto con un score, un reason y sub_flags para rasgos como direcciones de rol o desechables. Consulta la referencia de la API para conocer la forma exacta de los campos; aquí solo necesitas el estado para decidir. Rechazar undeliverable es el valor por defecto seguro; también podrías rechazar risky, o guardar el score y dejarla pasar — eso es una decisión de política, y la guía de registro serverless explica cómo de estricto ser en el punto de recogida.

Uniendo las capas para validar una dirección de correo en Flask#

El lugar idiomático para añadir una comprobación por campo en Flask-WTF es un método en línea validate_<nombredelcampo> en el formulario. WTForms añade ese método a la cadena de validadores del campo y lo ejecuta después de los validadores de la lista, de modo que, para cuando se dispara, Email(check_deliverability=True) ya ha resuelto la sintaxis y la consulta MX. Protege con field.errors para que el viaje de ida y vuelta a la API solo ocurra con las direcciones que superaron ambas capas más baratas:

from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email, ValidationError

class SignupForm(FlaskForm):
    email = StringField("Email", validators=[
        DataRequired(),
        Email(check_deliverability=True),   # layers 1 + 2
    ])

    def validate_email(self, field):        # layer 3 — runs after the list above
        if field.errors:                    # syntax or MX already failed
            return                          # don't spend an API credit
        if verify(field.data)["status"] == "undeliverable":
            raise ValidationError("We couldn't confirm a mailbox at this address.")

Esa protección con field.errors es todo el truco. Una dirección mal formada o un dominio muerto registra un error en la cadena antes de que se ejecute el método en línea, así que la protección detiene la llamada de verificación en las direcciones que nunca tuvieron oportunidad — validar en el orden equivocado, o saltarse la protección, gasta un crédito en cada errata. En una vista, validate_on_submit ejecuta todo el proceso antes de que se ejecute el cuerpo de tu handler:

@app.route("/signup", methods=["POST"])
def signup():
    form = SignupForm()
    if form.validate_on_submit():
        # every layer passed — safe to persist
        create_account(form.email.data)
        return redirect(url_for("welcome"))
    return render_template("signup.html", form=form), 400

Una cosa que decidir de antemano: qué pasa cuando la propia llamada a la API falla. Un timeout de red o una excepción de requests dentro de validate_email no debería devolver un 500 en la petición ni entregarle a un cliente real un error que no puede arreglar. Captura requests.RequestException por separado de ValidationError, y trata un servicio inalcanzable como unknown en lugar de undeliverable — deja pasar el registro y vuelve a verificar la dirección más tarde, en vez de bloquear el alta por una caída transitoria. Las capas de sintaxis y MX ya se ejecutaron, así que solo estás suavizando la capa que depende de la red.

No todas las aplicaciones de Flask usan WTForms. Para una API JSON, las mismas tres capas se leen directamente en una ruta, devolviendo un 400 en una dirección incorrecta en lugar de lanzar ValidationError:

from flask import Flask, request, jsonify
from email_validator import validate_email, EmailNotValidError

app = Flask(__name__)

@app.post("/api/signup")
def api_signup():
    email = (request.get_json(silent=True) or {}).get("email", "")
    try:
        info = validate_email(email, check_deliverability=True)  # layers 1 + 2
    except EmailNotValidError as exc:
        return jsonify(error=str(exc)), 400

    if verify(info.normalized)["status"] == "undeliverable":     # layer 3
        return jsonify(error="Email address appears undeliverable."), 400

    # ... create the account
    return jsonify(ok=True), 201

info.normalized es la dirección canonicalizada que email-validator devuelve, que es lo que deberías guardar. Esta es la misma estructura de tres capas que encontrarás en las versiones de Node.js y Python de esta guía — es la disposición en capas, no el framework, lo que hace que la validación funcione. Cuando estés eligiendo qué servicio de verificación respalda la capa tres, la comparativa de APIs alinea las opciones.

Preguntas frecuentes#

¿Basta con el validador Email de WTForms para validar una dirección de correo?#

Para la sintaxis, sí — Email() delega en el paquete email-validator y es una comprobación de primera capa mejor que cualquier expresión regular artesanal. Pero, por defecto, valida la forma, no la entregabilidad. Añade check_deliverability=True y también ejecutará una consulta MX en vivo, que descarta los dominios muertos; aun así, nunca contacta con el buzón, de modo que un resultado positivo significa «el dominio acepta correo», no «esta dirección existe». Combínalo con una comprobación del buzón por SMTP antes de fiarte de la dirección.

¿Cómo añado un validador de correo personalizado en Flask-WTF?#

Dos opciones idiomáticas. Para una comprobación puntual, añade un método en línea llamado validate_<nombredelcampo> al formulario — por ejemplo, validate_email(self, field) — y lanza wtforms.validators.ValidationError cuando el valor no sea válido; WTForms lo ejecuta después de los validadores enumerados en el campo. Para una regla reutilizable, escribe un invocable que reciba (form, field) y lance el mismo error, y luego pásalo en la lista validators del campo. Cualquiera de las dos es el lugar donde encaja la llamada de verificación de Qualisend.

¿Puedo comprobar si un buzón existe en Flask sin una API?#

En parte. email-validator con check_deliverability=True (o dnspython directamente) confirma que el dominio acepta correo, lo que descarta gratis los dominios muertos y no necesita nada más allá de una pequeña dependencia. Confirmar el buzón implica una conversación SMTP que puedes intentar con smtplib, pero que no deberías ejecutar desde el servidor de tu aplicación — el puerto 25 está bloqueado por todas partes y el resultado depende de la reputación de tu IP. Esa es la capa que un servicio de verificación existe para gestionar.

¿Debería validar los correos en el registro o al limpiar una lista?#

Ambas cosas, con distinta profundidad. Ejecuta las capas de sintaxis y MX de forma síncrona en el formulario o la ruta — son lo bastante rápidas como para bloquear la petición y dar feedback instantáneo — y actúa allí también según el veredicto de la API. Reserva la verificación por lotes, más pesada, para la limpieza de listas y las tareas de back-office, donde la latencia no importa y puedes procesar direcciones en bloque en lugar de un envío de formulario cada vez.


¿Listo para añadir la capa SMTP? Introduce una dirección aprobada por Email() en el comprobador de correo gratuito para ver cómo una cadena válida para WTForms vuelve como undeliverable, y luego conecta el mismo veredicto a tu formulario con la referencia de la API — con ejemplos para copiar y pegar incluidos.

Your reputation, protected.

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

Get started