Skip to content
Comece com 100 créditos de verificação grátis
Qualisend
Todos os artigos
Engenharia / 17 de junho de 2026

Como validar um endereço de e-mail em Python

5 min de leitura

Qualisend team
Como validar um endereço de e-mail em Python

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
is_valid_syntax("a..b@x.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, confira primeiro result["smtp"]: cached ou skipped significa que o resultado já é definitivo, e só pending exige fazer polling do job. Faça esse polling em uma tarefa em segundo plano, não dentro da requisição:

import time

def verify_and_wait(email: str, tries: int = 10, delay: float = 1.5):
    data = verify(email)
    if data["result"]["smtp"] != "pending":
        return data["result"]  # cached or skipped: already final
    job_id = data["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": ...}
        if job["status"] in ("canceled", "failed"):
            return None
        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/bulk em uma única requisição, receba de volta um único job_id e faça polling uma vez para o lote todo. Linhas duplicadas são descartadas (e reembolsadas) e os resultados vêm em páginas de até 1.000, então percorra as páginas e indexe cada linha pelo seu email:

def verify_bulk(emails: list[str], tries: int = 60, delay: float = 2.0):
    res = requests.post(f"{BASE}/verify/bulk", headers=HEADERS,
                        json={"text": "\n".join(emails), "expected_count": len(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}", headers=HEADERS, timeout=30).json()
        if job["status"] == "completed":
            return fetch_results(job_id)
        if job["status"] in ("canceled", "failed"):
            return None
        time.sleep(delay)  # back off; large lists take longer than a single probe
    return None

def fetch_results(job_id: str, page_size: int = 1000) -> dict:
    results, offset = {}, 0
    while True:
        page = requests.get(f"{BASE}/jobs/{job_id}", headers=HEADERS, timeout=30,
                            params={"include": "results", "limit": page_size,
                                    "offset": offset}).json()["results"]
        results.update({row["email"]: row for row in page})
        if len(page) < page_size:
            return results
        offset += page_size

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"}
    try:
        return verify(email)["result"]  # deliverable | risky | undeliverable | unknown
    except requests.RequestException:  # timeout, 402, 429, 5xx: fail open
        return {"status": "unknown", "reason": None}  # let it through, re-verify later

Se a própria chamada à API falhar (timeout, 402 por falta de créditos, 429 por limite de taxa ou um 5xx), a função libera o endereço com unknown: deixe o cadastro passar e verifique o endereço de novo mais tarde.

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

01Dá 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.

02Por 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.

03O 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.

04O 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.

Sua reputação, protegida.

Limpe sua primeira lista em minutos. 100 créditos grátis, sem cartão.

Começar