Valider une adresse e-mail en Python revient à effectuer trois contrôles distincts, et celui que la plupart des tutoriels présentent — une regex — est le moins utile de tous. La vraie validation est en couches : la syntaxe, puis une requête DNS, puis un test SMTP de la boîte aux lettres. La bibliothèque standard de Python et une petite dépendance couvrent les deux premières ; la troisième est un problème réseau qu'il vaut mieux déléguer. Ce guide construit chaque couche avec du code fonctionnel.
La réponse courte#
Utilisez une regex permissive (ou email.utils.parseaddr) pour la syntaxe, dnspython
pour la requête MX et une API de vérification pour le contrôle SMTP de la boîte aux
lettres — du moins coûteux au plus coûteux, en s'arrêtant dès qu'un contrôle est
décisif. N'allez pas chercher smtplib pour sonder les boîtes aux lettres depuis votre
application : le port sortant 25 est bloqué sur la plupart des hébergeurs, et le résultat
dépend de la réputation de l'IP d'envoi et du
greylisting que vous ne voulez pas réimplémenter.
Couche 1 : la syntaxe#
Restez permissif — repérez les fautes de frappe, ne réimplémentez pas la RFC 5322 (ce qui n'aiderait de toute façon pas) :
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
La fonction email.utils.parseaddr de la bibliothèque standard analyse bien une
adresse, mais elle est indulgente par conception — parseaddr("nonsense") renvoie
('', 'nonsense') sans lever d'exception — c'est donc un analyseur, pas un validateur.
Une courte regex est plus claire pour un filtre de syntaxe binaire (oui/non).
Couche 2 : le domaine peut-il recevoir du courrier ?#
Un domaine sans enregistrement MX ne peut accepter de courrier pour personne. dnspython
les résout en quelques lignes :
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
Cette seule requête élimine gratuitement les domaines morts et les TLD inventés. Si vous
souhaitez honorer les domaines qui acceptent le courrier sur un enregistrement A sans MX,
repliez-vous sur la résolution de A/AAAA lorsque l'ensemble MX est vide — mais un
contrôle MX couvre la grande majorité des adresses réelles.
Couche 3 : la boîte aux lettres existe-t-elle vraiment ?#
Les couches 1 et 2 ne peuvent qu'écarter une adresse. Confirmer une boîte aux lettres
suppose la conversation de remise SMTP — et si smtplib peut en ouvrir une, l'exécuter
depuis votre application est une mauvaise idée : le port 25 est largement bloqué, la
réponse dépend de la réputation de l'IP depuis laquelle vous vous connectez, et les
serveurs de réception appliquent du greylisting et une limitation de débit aux inconnus.
Déléguez cette couche.
Le POST /verify de Qualisend exécute le pipeline complet. Les contrôles
locaux répondent immédiatement, le test SMTP étant mis en file d'attente :
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()
Pour la validation en direct à l'inscription, le result immédiat suffit généralement
pour agir. Lorsque vous avez besoin du verdict confirmé par SMTP, interrogez la tâche
jusqu'à ce que le test se termine :
import time
def verify_and_wait(email: str, tries: int = 10, delay: float = 1.5):
job_id = verify(email)["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": ...}
time.sleep(delay)
return None # still processing — treat as unknown, retry later
Vérifier une liste en masse#
Ne bouclez pas verify() sur un CSV adresse par adresse — cela représente un aller-retour
HTTP et une tâche par ligne. Envoyez la liste entière à /verify en une seule requête,
récupérez un unique job_id, et interrogez une seule fois pour l'ensemble du lot. Le
tableau results revient dans l'ordre de soumission :
def verify_bulk(emails: list[str], tries: int = 60, delay: float = 2.0):
res = requests.post(f"{BASE}/verify", headers=HEADERS,
json={"emails": 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}",
params={"include": "results"}, headers=HEADERS,
timeout=30).json()
if job["status"] == "completed":
return dict(zip(emails, job["results"]))
time.sleep(delay) # back off; large lists take longer than a single probe
return None
Un lot de plusieurs milliers d'adresses peut prendre plusieurs minutes, car chaque test
SMTP est une conversation réseau distincte ; élargissez donc tries/delay pour les
listes plus volumineuses plutôt que de marteler le point de terminaison d'interrogation.
Exécutez d'abord les deux couches locales pour écarter les déchets évidents avant de
dépenser des crédits — filtrez avec is_valid_syntax et has_mail_route, puis n'envoyez
que les survivants.
Notes sur l'asynchrone et pydantic#
requests est bloquant : dans une application asyncio, enveloppez donc l'appel avec
asyncio.to_thread(verify, email) ou passez à httpx.AsyncClient pour déclencher les
appels de vérification en parallèle ; dnspython fournit un résolveur asynchrone
(dns.asyncresolver) pour la même raison. Si vous utilisez déjà pydantic, son type
EmailStr remplace proprement la couche 1 — il valide la syntaxe à la construction d'un
modèle — mais il s'arrête là : il ne touche jamais au DNS ni à la boîte aux lettres,
gardez donc le contrôle MX et l'API de vérification en aval de celui-ci.
Assembler les couches#
Du moins coûteux au plus coûteux, en s'arrêtant dès que vous avez une réponse :
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"}
return verify(email)["result"] # deliverable | risky | undeliverable | unknown
Les deux couches locales ne coûtent rien et interceptent instantanément la plupart des déchets ; l'API ne s'exécute que sur les adresses qui valent l'aller-retour. Voyez comment fonctionne la vérification d'e-mails pour comprendre pourquoi les étapes se succèdent dans cet ordre.
Foire aux questions#
Puis-je vérifier un e-mail en Python avec la seule bibliothèque standard ?#
Jusqu'à un certain point. re gère la syntaxe et, avec socket, vous pouvez tenter une
requête DNS — mais il n'existe pas de résolveur MX intégré (d'où dnspython), et confirmer
une boîte aux lettres nécessite un test SMTP que vous ne devriez pas exécuter depuis votre
application. La bibliothèque standard vous permet de contrôler la syntaxe et d'effectuer une
vérification approximative du domaine ; la couche « boîte aux lettres » exige dnspython
ainsi qu'un service de vérification.
Pourquoi ne pas utiliser smtplib pour vérifier qu'une boîte aux lettres existe ?#
smtplib peut ouvrir la conversation SMTP, mais l'exécuter depuis votre serveur applicatif
n'est ni fiable ni sans risque : la plupart des hébergeurs bloquent le port sortant 25, la
réponse dépend de la réputation de votre IP d'envoi, et sonder à un certain volume vous vaut
d'être limité en débit ou inscrit sur une liste de blocage. Une API de vérification exécute
le test depuis une infrastructure à la réputation gérée, conçue pour cela.
dnspython est-elle la bonne bibliothèque pour les requêtes MX ?#
Oui — c'est la boîte à outils DNS standard et bien maintenue pour Python, et elle résout
proprement les enregistrements MX. Installez-la avec pip install dnspython et interceptez
NXDOMAIN, NoAnswer et NoNameservers pour considérer une route de messagerie absente
comme non délivrable.
Le type EmailStr de pydantic valide-t-il entièrement une adresse e-mail ?#
Non — EmailStr (qui s'appuie sur le paquet email-validator) se contente de vérifier
qu'une chaîne constitue syntaxiquement une adresse valide au moment de la construction de
votre modèle. Il ne confirme ni que le domaine possède des enregistrements MX, ni que la
boîte aux lettres existe : il correspond donc exactement à la couche 1. Utilisez-le comme
filtre de syntaxe dans vos modèles pydantic, puis exécutez la requête DNS et l'API de
vérification sur les adresses qui passent. Il normalise également l'adresse (met le domaine
en minuscules), ce qui s'avère pratique avant de dédupliquer une liste.
Prêt à ajouter la couche SMTP ? Le forfait gratuit inclut 100 crédits qui
exécutent le pipeline complet, et la référence de l'API présente les points
de terminaison /verify et /jobs au complet, avec des exemples prêts à copier-coller
dans sept langages.