Validar email em Python sem reinventar a roda
email-validator, validators, dnspython e a API do EmailChecker: valide emails em Django, FastAPI e Flask sem escrever regex do zero.
Todo desenvolvedor Python já escreveu (ou copiou do Stack Overflow) aquela regex enorme pra validar email. Spoiler: ela estava errada. O formato de um endereço de email é definido pela RFC 5321/5322 e tem casos de borda suficientes pra deixar qualquer regex com saudade dos anos 90. Neste post vou mostrar as abordagens disponíveis, com código real, e terminar com um padrão que funciona em produção pra Django form e FastAPI Pydantic.
Visão geral: três camadas de validação
Antes de instalar qualquer coisa, vale entender o que cada abordagem entrega:
| Camada | O que valida | Custo |
|---|---|---|
| Regex / sintaxe | Formato RFC (local-part, @, domain) | Zero — local, sem I/O |
| DNS (MX lookup) | Domínio aceita emails — servidor MX existe | Latência de rede, sem custo financeiro |
| SMTP probe | Caixa existe de fato no servidor | Maior latência, bloqueado por muitos servidores |
| API externa | Sintaxe + DNS + heurísticas (disposable, catch-all, risk score) | Latência de rede + custo por consulta |
A regra prática: em cadastro de usuário, valide sintaxe no front-end, faça MX lookup no back-end antes de persistir, e chame API externa apenas em fluxos sensíveis (leads pagos, onboarding crítico). As seções a seguir cobrem cada nível.
Lib 1: email-validator — RFC compliance de verdade
A biblioteca email-validator é a escolha padrão quando você quer validação RFC sem depender de rede. Ela implementa a RFC 5321/5322 corretamente, normaliza o endereço e lida com internacionalização (SMTPUTF8).
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install email-validator
Uso básico:
from email_validator import validate_email, EmailNotValidError
def validar_email(endereco: str) -> dict:
try:
info = validate_email(endereco, check_deliverability=False)
# info.normalized retorna o endereço normalizado
return {"valido": True, "normalizado": info.normalized}
except EmailNotValidError as e:
return {"valido": False, "erro": str(e)}
# exemplos
print(validar_email("usuario@exemplo.com.br"))
# {'valido': True, 'normalizado': 'usuario@exemplo.com.br'}
print(validar_email("usuário@exemplo.com"))
# aceita UTF-8 no local-part se o servidor suportar SMTPUTF8
print(validar_email("nao-email"))
# {'valido': False, 'erro': 'The email address is not valid...'}
O parametro check_deliverability=True (padrão) faz um MX lookup automaticamente — o que pode ser exatamente o que você quer sem precisar de outra lib:
from email_validator import validate_email, EmailNotValidError
def validar_com_dns(endereco: str) -> dict:
try:
# check_deliverability=True faz MX lookup
info = validate_email(endereco, check_deliverability=True)
return {
"valido": True,
"normalizado": info.normalized,
"dominio": info.domain,
}
except EmailNotValidError as e:
return {"valido": False, "erro": str(e)}
Para Django, a validate_email do django.core.validators já usa internamente uma lógica parecida, mas sem MX lookup. Se quiser o lookup, troque pelo wrapper acima.
Lib 2: validators — leve e sem cerimônia
A lib validators é boa quando você precisa validar email junto de URLs, IPs, cartoes de crédito e não quer manter cinco dependências separadas. O trade-off é que ela é menos rigorosa com a RFC e não faz MX lookup.
pip install validators
import validators
def checar_email(endereco: str) -> bool:
resultado = validators.email(endereco)
# retorna True em caso de sucesso, ValidationError em caso de falha
return resultado is True
print(checar_email("dev@exemplo.com.br")) # True
print(checar_email("sem-arroba.com")) # False
Use validators quando: o formulário já tem outros campos que se beneficiam da lib (URL, CPF via plugin, etc.) e você não precisa de MX lookup nem normalização UTF-8. Para validação de email isolada, email-validator é mais completa.
DIY: MX lookup com dnspython
Se precisar do lookup sem depender do check_deliverability do email-validator, o dnspython dá controle total: timeout, servidor DNS customizado, tratamento granular de erros.
pip install dnspython
import dns.resolver
import dns.exception
def dominio_tem_mx(dominio: str, timeout: float = 5.0) -> bool:
"""
Retorna True se o domínio tem ao menos um registro MX.
Não garante que a caixa existe — apenas que o domínio aceita emails.
"""
try:
respostas = dns.resolver.resolve(dominio, "MX", lifetime=timeout)
return len(respostas) > 0
except (
dns.resolver.NXDOMAIN, # domínio não existe
dns.resolver.NoAnswer, # sem registros MX
dns.resolver.NoNameservers, # DNS indisponível
dns.exception.Timeout,
):
return False
# uso combinado com email-validator (só sintaxe) + lookup próprio
from email_validator import validate_email, EmailNotValidError
def validar_completo(endereco: str) -> dict:
try:
info = validate_email(endereco, check_deliverability=False)
except EmailNotValidError as e:
return {"valido": False, "etapa": "sintaxe", "erro": str(e)}
if not dominio_tem_mx(info.domain):
return {
"valido": False,
"etapa": "dns",
"erro": f"Domínio {info.domain} não tem registro MX",
}
return {"valido": True, "normalizado": info.normalized}
A vantagem dessa abordagem: você controla o timeout (útil em Django com CONN_MAX_AGE) e pode usar um resolver alternativo (ex.: 1.1.1.1) sem alterar configuração do sistema.
API EmailChecker: score + heurísticas avancadas
MX lookup confirma que o domínio aceita emails, mas não detecta endereços descartáveis (Mailinator, Guerrilla Mail), domínios catch-all ou risco de bounce. Para isso, a API do EmailChecker retorna um score e flags adicionais.
Veja os exemplos completos em Python na documentação.
A API do EmailChecker é assíncrona e em lote: você envia os endereços num POST, recebe o id do lote e consulta o resultado depois. Não existe endpoint que devolva o veredito na mesma resposta — validar caixa de verdade exige falar com o servidor do destinatário, e isso não cabe no tempo de um request HTTP.
Enviando um lote com requests
pip install requests
import time
import requests
API_KEY = "ec_live_sua_chave_aqui"
BASE_URL = "https://app.emailchecker.email/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
def enviar_lote(enderecos: list[str], nome: str = "lote-python") -> str:
resp = requests.post(
f"{BASE_URL}/batch",
json={"emails": enderecos, "name": nome},
headers=HEADERS,
timeout=30,
)
resp.raise_for_status()
return resp.json()["data"]["id"]
def buscar_resultado(batch_id: str, intervalo: int = 5, tentativas: int = 120) -> list[dict]:
for _ in range(tentativas):
resp = requests.get(f"{BASE_URL}/batch/{batch_id}", headers=HEADERS, timeout=15)
resp.raise_for_status()
dados = resp.json()["data"]
if dados["status"] == "completed":
return dados["emails"]
if dados["status"] in ("failed", "cancelled"):
raise RuntimeError(f"lote terminou como {dados['status']}")
time.sleep(intervalo)
raise TimeoutError("lote não finalizou no tempo esperado")
# uso
batch_id = enviar_lote(["contato@empresa.com.br", "typo@gmial.com"])
for item in buscar_resultado(batch_id):
print(item["email"], item["result"], item["score"])
Cada item de emails traz result (deliverable, undeliverable, risky ou unknown), score de 0 a 100, reason e os flags detalhados (isv_format, isv_domain, isv_mx, isv_nocatchall, is_free).
Versão assíncrona com httpx
Em aplicações FastAPI ou Starlette, use o cliente assíncrono do httpx para não bloquear o event loop enquanto espera o lote:
import asyncio
import httpx
API_KEY = "ec_live_sua_chave_aqui"
BASE_URL = "https://app.emailchecker.email/api/v1"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
async def validar_lote(enderecos: list[str]) -> list[dict]:
async with httpx.AsyncClient(timeout=30.0, headers=HEADERS) as client:
resp = await client.post(f"{BASE_URL}/batch", json={"emails": enderecos})
resp.raise_for_status()
batch_id = resp.json()["data"]["id"]
while True:
resp = await client.get(f"{BASE_URL}/batch/{batch_id}")
resp.raise_for_status()
dados = resp.json()["data"]
if dados["status"] == "completed":
return dados["emails"]
if dados["status"] in ("failed", "cancelled"):
raise RuntimeError(f"lote terminou como {dados['status']}")
await asyncio.sleep(5)
No cadastro, o padrão que funciona é: aceitar o usuário na hora (com a checagem de sintaxe local que vimos acima) e mandar o endereço pra fila de validação. Quando o resultado chegar, você marca o contato como confirmado, suspeito ou inválido.
Para mais detalhes sobre rate limits, autenticação e campos de resposta, consulte o [guia de validação de email](/guia/validacao-de-email).
---
## Comparacao de trade-offs
| Abordagem | Velocidade | Precisão | Custo infra | Depende de rede |
|---|---|---|---|---|
| `email-validator` (sem DNS) | Muito rapida | Sintaxe RFC | Zero | Nao |
| `validators` | Muito rapida | Basica | Zero | Nao |
| `email-validator` + MX | Rapida (50-200ms) | Boa | Zero | Sim |
| `dnspython` manual | Rapida (configuravel) | Boa | Zero | Sim |
| API EmailChecker | Media (100-400ms) | Alta (score, disposable, catch-all) | Por consulta | Sim |
Regra pratica:
- Formularios publicos com alto volume: `email-validator` com `check_deliverability=True` — cobre 90% dos casos sem custo.
- Onboarding critico, leads pagos, envio de email transacional: API EmailChecker com score >= 70 como threshold de aceite.
- Nunca coloque chamada de API externa no caminho critico de um endpoint sem timeout e fallback.
---
## Padrao recomendado: Django form e FastAPI Pydantic
### Django — metodo clean no ModelForm
```python
# forms.py
from django import forms
from email_validator import validate_email, EmailNotValidError
class CadastroForm(forms.ModelForm):
class Meta:
model = Usuario
fields = ["email", "nome"]
def clean_email(self):
endereco = self.cleaned_data["email"]
try:
info = validate_email(endereco, check_deliverability=True)
except EmailNotValidError as e:
raise forms.ValidationError(str(e))
# persiste o endereco normalizado, nao o que o usuario digitou
return info.normalized
Se precisar do score da API (ex.: em formulario de lead), chame dentro do clean_email mas com try/except largo — nunca deixe um timeout de API quebrar o submit do usuario.
FastAPI — Pydantic validator
# schemas.py
from pydantic import BaseModel, field_validator
from email_validator import validate_email, EmailNotValidError
class LeadCreate(BaseModel):
email: str
nome: str
@field_validator("email")
@classmethod
def normalizar_email(cls, v: str) -> str:
try:
info = validate_email(v, check_deliverability=True)
return info.normalized
except EmailNotValidError as e:
raise ValueError(str(e))
# router.py
from fastapi import APIRouter
from .schemas import LeadCreate
router = APIRouter()
@router.post("/leads")
async def criar_lead(payload: LeadCreate):
# payload.email ja esta normalizado e com MX validado
...
Para FastAPI com validacao de API externa, use um BackgroundTask ou Celery task para nao bloquear a resposta — rejeite o lead apenas se houver evidencia forte (disposable=True, score < 30), e processe o restante de forma assincrona.
Conclusao
Nao existe motivo para escrever regex de email em 2026. A email-validator com check_deliverability=True cobre a grande maioria dos casos com uma linha, normaliza o endereco e ainda faz o MX lookup. Para casos que exigem mais profundidade — deteccao de enderecos descartaveis, catch-all, score de risco — a API do EmailChecker complementa sem substituir a validacao local. O mesmo raciocinio em camadas, aplicado ao ecossistema JavaScript, esta em validar email em Node.js: 4 abordagens com codigo.
O stack recomendado em ordem de complexidade crescente:
email-validator— sempre, na primeira linha do validator.dnspython— quando voce precisa de controle fino sobre o lookup (timeout, resolver customizado).- API EmailChecker — para fluxos criticos onde a qualidade do email tem impacto financeiro direto.
Instale as dependencias, configure o virtualenv, e pare de reinventar a roda.
pip install email-validator dnspython httpx requests
Referências: