EmailCheckerEmailChecker

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.

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.

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-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

# 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:

  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.