EmailCheckerEmailChecker

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.

Por João Costa (Engenharia)·Publicado em 27/05/2026·8 min de leitura

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:

  1. email-validator — sempre, na primeira linha do validator.
  2. dnspython — quando voce precisa de controle fino sobre o lookup (timeout, resolver customizado).
  3. 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:

Comece agora

Pronto pra parar de mandar email pra endereço morto?

Comece grátis com 500 créditos. Sem cartão, sem compromisso.