Skip to content
Commencez avec 100 crédits de vérification gratuits
Qualisend
Tous les articles
Ingénierie / 17 juin 2026

Comment valider une adresse e-mail en Python

5 minutes read

Qualisend team
Une fenêtre de code Python validant un e-mail via les couches syntaxe, DNS et SMTP

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.

Your reputation, protected.

Clean your first list in minutes. 100 free credits, no card required.

Get started