Validar um endereço de e-mail em Python significa três verificações separadas, e a que a maioria dos tutoriais mostra — uma regex — é a menos útil de todas. A validação de verdade é feita em camadas: sintaxe, depois uma consulta de DNS e, então, um teste de caixa postal via SMTP. A biblioteca padrão do Python e uma pequena dependência dão conta das duas primeiras; a terceira é um problema de rede que vale a pena delegar. Este guia constrói cada camada com código funcional.
A resposta curta#
Use uma regex permissiva (ou email.utils.parseaddr) para a sintaxe, o dnspython
para a consulta de MX e uma API de verificação para a checagem de caixa postal via
SMTP — a mais barata primeiro, encerrando o processo assim que uma delas for decisiva.
Não recorra ao smtplib para sondar caixas postais a partir da sua aplicação: a porta
25 de saída fica bloqueada na maioria dos provedores, e o resultado depende da reputação
do IP de envio e do greylisting que você não quer
reimplementar.
Camada 1: sintaxe#
Mantenha-a permissiva — pegue erros de digitação, não reimplemente a RFC 5322 (o que não ajudaria mesmo):
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
O email.utils.parseaddr da biblioteca padrão até analisa um endereço, mas é
tolerante por design — parseaddr("nonsense") retorna ('', 'nonsense') sem
levantar erro — então é um analisador, não um validador. Uma regex curta é mais clara
como barreira de sintaxe do tipo sim/não.
Camada 2: o domínio consegue receber e-mails?#
Um domínio sem registros MX não consegue aceitar e-mail de ninguém. O dnspython
os resolve em algumas linhas:
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
Essa única consulta elimina domínios mortos e TLDs inventados de graça. Se você quiser
contemplar domínios que aceitam e-mail em um registro A sem MX, recorra a resolver
A/AAAA quando o conjunto de MX estiver vazio — mas a verificação de MX cobre a
grande maioria dos endereços reais.
Camada 3: a caixa postal realmente existe?#
As Camadas 1 e 2 só conseguem descartar um endereço. Confirmar uma caixa postal
significa a conversa de entrega via SMTP — e, embora o smtplib consiga abrir uma,
rodá-lo a partir da sua aplicação é uma má ideia: a porta 25 é amplamente bloqueada,
a resposta depende da reputação do IP de onde você se conecta, e os servidores que
recebem aplicam greylisting e limitam a taxa de estranhos. Delegue esta camada.
O POST /verify da Qualisend executa o pipeline completo. As
verificações locais retornam de imediato, com o teste SMTP em fila:
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 validação de cadastro ao vivo, o result imediato costuma ser suficiente para
agir. Quando você precisar do veredito confirmado por SMTP, faça polling do job até
o teste terminar:
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
Verificando uma lista em massa#
Não faça um loop de verify() sobre um CSV, um endereço por vez — isso é uma ida e
volta HTTP e um job por linha. Envie a lista inteira para o /verify em uma única
requisição, receba de volta um único job_id e faça polling uma vez para o lote todo.
O array results volta na ordem de submissã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
Um lote de milhares pode levar minutos, porque cada teste SMTP é uma conversa de rede
separada, então amplie tries/delay para listas maiores em vez de martelar o
endpoint de polling. Rode as duas camadas locais primeiro para descartar o lixo óbvio
antes de gastar créditos — filtre com is_valid_syntax e has_mail_route e, depois,
envie apenas os sobreviventes.
Notas sobre async e pydantic#
O requests é bloqueante, então, em uma aplicação asyncio, envolva a chamada com
asyncio.to_thread(verify, email) ou troque para httpx.AsyncClient para disparar as
chamadas de verificação de forma concorrente; o dnspython traz um resolvedor async
(dns.asyncresolver) pelo mesmo motivo. Se você já usa pydantic, o tipo EmailStr
é um substituto limpo para a Camada 1 — ele valida a sintaxe quando um modelo é
construído —, mas para por aí: nunca toca no DNS nem na caixa postal, então mantenha a
verificação de MX e a API de verificação a jusante dele.
Juntando as camadas#
A mais barata primeiro, parando assim que você tiver uma resposta:
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
As duas camadas locais não custam nada e pegam a maior parte do lixo na hora; a API roda apenas nos endereços que valem a ida e volta. Veja como funciona a verificação de e-mail para entender por que as etapas ficam nesta ordem.
Perguntas frequentes#
Dá para verificar um e-mail em Python usando só a biblioteca padrão?#
Até certo ponto. O re resolve a sintaxe e, com socket, você pode tentar o DNS — mas
não existe um resolvedor de MX embutido (por isso o dnspython), e confirmar uma caixa
postal exige um teste SMTP que você não deveria rodar a partir da sua aplicação. A
biblioteca padrão dá conta da sintaxe e de uma verificação de domínio superficial; a
camada da caixa postal precisa do dnspython mais um serviço de verificação.
Por que não usar o smtplib para checar se uma caixa postal existe?#
O smtplib consegue abrir a conversa SMTP, mas rodá-lo a partir do servidor da sua
aplicação é pouco confiável e arriscado: a maioria dos provedores bloqueia a porta 25
de saída, a resposta depende da reputação do seu IP de envio, e sondar em qualquer
volume acaba te levando a limitação de taxa ou à blocklist. Uma API de verificação
executa o teste a partir de uma infraestrutura com reputação gerenciada, feita para
isso.
O dnspython é a biblioteca certa para consultas de MX?#
Sim — é o kit de ferramentas de DNS padrão e bem mantido para Python, e resolve
registros MX de forma limpa. Instale com pip install dnspython e capture NXDOMAIN,
NoAnswer e NoNameservers para tratar a ausência de rota de e-mail como não
entregável.
O EmailStr do pydantic valida um endereço de e-mail por completo?#
Não — o EmailStr (baseado no pacote email-validator) só verifica se uma string é
sintaticamente um endereço válido no momento em que o seu modelo é construído. Ele não
confirma se o domínio tem registros MX nem se a caixa postal existe, então corresponde
exatamente à Camada 1. Use-o como a barreira de sintaxe nos seus modelos pydantic e,
depois, rode a consulta de DNS e a API de verificação nos endereços que passarem. Ele
também normaliza o endereço (deixa o domínio em minúsculas), o que ajuda antes de você
remover duplicatas de uma lista.
Pronto para adicionar a camada SMTP? O plano gratuito inclui 100 créditos
que rodam o pipeline completo, e a referência da API traz os endpoints
/verify e /jobs completos, com exemplos de copiar e colar em sete linguagens.