Validar email em Python sem reinventar a roda
Guia prático com email-validator, validators, dnspython e a API do EmailChecker para validar endereços de email 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.
Chamada síncrona com requests
pip install requests
import requests
EMAILCHECKER_API_KEY = "sua-chave-aqui"
EMAILCHECKER_ENDPOINT = "https://api.emailchecker.com.br/v1/validate"
def verificar_email_api(endereco: str) -> dict:
try:
resp = requests.get(
EMAILCHECKER_ENDPOINT,
params={"email": endereco},
headers={"Authorization": f"Bearer {EMAILCHECKER_API_KEY}"},
timeout=8,
)
resp.raise_for_status()
dados = resp.json()
return {
"valido": dados.get("valid"),
"score": dados.get("score"), # 0-100
"descartavel": dados.get("disposable"),
"catch_all": dados.get("catch_all"),
"normalizado": dados.get("normalized"),
}
except requests.Timeout:
# fallback: aceitar e verificar de forma assíncrona depois
return {"valido": None, "erro": "timeout"}
except requests.HTTPError as e:
return {"valido": None, "erro": str(e)}
Chamada assíncrona com httpx
Em aplicações FastAPI ou Starlette, use o cliente assíncrono do httpx para não bloquear o event loop:
import httpx
EMAILCHECKER_API_KEY = "sua-chave-aqui"
EMAILCHECKER_ENDPOINT = "https://api.emailchecker.com.br/v1/validate"
async def verificar_email_api_async(endereco: str) -> dict:
async with httpx.AsyncClient(timeout=8.0) as client:
try:
resp = await client.get(
EMAILCHECKER_ENDPOINT,
params={"email": endereco},
headers={"Authorization": f"Bearer {EMAILCHECKER_API_KEY}"},
)
resp.raise_for_status()
dados = resp.json()
return {
"valido": dados.get("valid"),
"score": dados.get("score"),
"descartavel": dados.get("disposable"),
"normalizado": dados.get("normalized"),
}
except httpx.TimeoutException:
return {"valido": None, "erro": "timeout"}
except httpx.HTTPStatusError as e:
return {"valido": None, "erro": str(e)}
Para mais detalhes sobre rate limits, autenticação e campos de resposta, consulte o guia de validação 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-validatorcomcheck_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
# 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 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: