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

Comment valider une adresse e-mail dans Flask

9 minutes read

Qualisend team
Fenêtre de code intitulée forms.py montrant trois couches de validation Flask de plus en plus fines — Email(), une résolution MX check_deliverability et une requête POST vers l'API verify — se terminant par un badge vert « deliverable ».

Pour valider une adresse e-mail dans Flask, vous effectuez trois contrôles, pas un seul — et Flask lui-même n'en fournit aucun, car Flask ne dispose pas de sa propre couche de formulaires. Ce qu'il possède, c'est un écosystème : WTForms (généralement via Flask-WTF) pour la gestion des formulaires, et le paquet email-validator sur lequel WTForms s'appuie. Ensemble, ils répondent aux deux premières des trois questions de la validation — l'adresse est-elle bien formée, et son domaine peut-il recevoir du courrier — mais jamais à la troisième : la boîte aux lettres existe-t-elle réellement. La vraie validation se fait par couches : syntaxe, puis résolution DNS, puis sondage SMTP de la boîte aux lettres. Ce guide construit chaque couche sous forme d'un validateur WTForms que vous pouvez insérer dans un formulaire Flask-WTF ou une simple route JSON, et montre où une API de vérification prend le relais. Il reprend là où s'arrête le guide Python, si bien que le raisonnement DNS et SMTP qui s'y trouve reste intégralement valable.

La réponse en bref#

Utilisez le validateur Email() de WTForms pour la syntaxe, son option check_deliverability=True (ou dnspython) pour la résolution MX, et une API de vérification pour le contrôle SMTP de la boîte aux lettres — câblés du moins cher au plus cher pour que chaque couche court-circuite avant que la suivante ne s'exécute. N'ouvrez pas de connexions SMTP depuis votre processus Flask pour sonder les boîtes aux lettres : le port 25 sortant est bloqué sur la plupart des hébergeurs, et la réponse dépend de la réputation de l'IP d'envoi et du greylisting que vous n'avez pas envie de réimplémenter. Chaque couche écarte des adresses de façon plus économique que la précédente ; seule l'API peut en valider une. Si ce pipeline vous est nouveau, ce qu'est la vérification d'e-mails en présente d'abord les termes.

Couche 1 : validation du format avec le validateur Email de WTForms#

WTForms se charge de la première couche, et il ne la bricole pas à la main. wtforms.validators.Email délègue au paquet email-validator : déclarer un validateur Email() sur un champ vous offre donc un vrai contrôle de syntaxe dérivé de la RFC — pas une expression régulière — gratuitement :

from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email

class SignupForm(FlaskForm):
    email = StringField("Email", validators=[DataRequired(), Email()])

Le validateur Email() nécessite l'installation du paquet (pip install email-validator, ou pip install wtforms[email]) ; sans lui, WTForms lève une erreur au moment de la validation pour vous le signaler. En interne, il appelle email_validator.validate_email, que vous pouvez aussi utiliser seul — hors d'un formulaire, ou dans une couche de service — pour obtenir le même verdict sous forme de booléen :

from email_validator import validate_email, EmailNotValidError

def is_valid_syntax(email: str) -> bool:
    try:
        validate_email(email, check_deliverability=False)
        return True
    except EmailNotValidError:
        return False

Sachez exactement ce que cela vous apporte. Avec check_deliverability=False, le contrôle ne lit que la forme — il ne résout jamais le DNS, n'ouvre jamais de socket, et ignore totalement si example.com existe. definitely-fake@gmail.com passe. info@company-that-folded.com passe. typo@gmial.com passe. Toutes trois sont non délivrables, et aucun validateur qui se contente de lire la chaîne ne vous le dira jamais, pour la même raison qui fait que la validation d'e-mail par regex échoue : syntaxe et délivrabilité sont deux questions différentes. L'une est un fait à propos de la chaîne ; l'autre est un fait à propos d'Internet.

Couche 2 : le domaine accepte-t-il le courrier ?#

Un domaine sans enregistrement MX ne peut recevoir de courrier pour personne : une seule résolution DNS élimine donc les domaines morts, les noms d'entreprise mal orthographiés et les TLD inventés. Ici, la pile de Flask fait quelque chose que celle de Django ne fait pas d'emblée : email-validator peut effectuer la résolution pour vous. Activez check_deliverability et il résout les enregistrements MX du domaine (avec repli sur A/AAAA) grâce à dnspython, en levant EmailUndeliverableError — une sous-classe de EmailNotValidError — lorsque le domaine ne peut pas recevoir de courrier. Sur un formulaire, cela tient à une option sur le validateur que vous avez déjà :

email = StringField("Email", validators=[DataRequired(), Email(check_deliverability=True)])

Cet unique indicateur fond les couches un et deux en un seul validateur. En usage autonome, le même appel renvoie un objet d'adresse normalisée lorsque le domaine est validé :

from email_validator import validate_email, EmailNotValidError

def has_mail_route(email: str) -> bool:
    try:
        validate_email(email, check_deliverability=True)  # syntax + live MX lookup
        return True
    except EmailNotValidError:
        return False

Si vous préférez effectuer le contrôle DNS vous-même — pour mettre les résultats en cache, pointer vers votre propre résolveur, ou fixer un délai d'expiration serré — descendez directement vers dnspython :

import dns.resolver  # pip install dnspython

def domain_has_mx(domain: str) -> bool:
    try:
        return len(dns.resolver.resolve(domain, "MX")) > 0
    except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
        return False

Couche 3 : la boîte aux lettres existe-t-elle réellement ?#

Les couches un et deux ne peuvent qu'écarter une adresse. Un domaine peut publier des enregistrements MX parfaits et n'avoir malgré tout aucune boîte aux lettres à l'adresse que vous détenez — noreply-9f2x@gmail.com est syntaxiquement valide sur une route de courrier active, et c'est pourtant une boîte aux lettres qui n'a jamais été créée. Confirmer une boîte aux lettres précise suppose le dialogue de livraison SMTP : se connecter à l'hôte de messagerie, émettre RCPT TO, lire la réponse, et se déconnecter avant d'envoyer quoi que ce soit.

En principe, vous pourriez scripter cela depuis Flask avec smtplib. En pratique, vous ne devriez pas l'exécuter depuis votre serveur applicatif : la plupart des hébergeurs cloud bloquent le port 25 sortant, la réponse dépend de la réputation de l'IP depuis laquelle vous vous connectez, et les serveurs de réception appliquent greylisting et limitation de débit aux expéditeurs inconnus — si bien qu'un sondage qui fonctionne dans un test local échoue silencieusement, ou vous fait blacklister, en production. Comment fonctionne la vérification d'e-mails parcourt le pipeline complet, domaines catch-all inclus. C'est la couche qui vaut la peine d'être déléguée.

Le point de terminaison verify de Qualisend exécute tout le pipeline — syntaxe, DNS et sondage SMTP de la boîte aux lettres — depuis une infrastructure à réputation gérée, et renvoie un verdict. Appelez-le avec requests, en lisant votre clé depuis l'environnement :

import os
import requests  # pip install requests

QUALISEND_VERIFY_URL = "https://api.qualisend.com/v1/verify"

def verify(email: str) -> dict:
    res = requests.post(
        QUALISEND_VERIFY_URL,
        headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
        json={"email": email},
        timeout=10,
    )
    res.raise_for_status()
    return res.json()["result"]  # {"status", "score", "reason", "sub_flags"}

La réponse est une enveloppe { "result": { ... } }. Lisez result["status"]deliverable, risky, undeliverable ou unknown — aux côtés d'un score, d'une reason et de sub_flags pour des traits comme les adresses de rôle ou jetables. Consultez la référence de l'API pour la forme exacte des champs ; ici vous n'avez besoin que du statut pour décider. Rejeter undeliverable est le choix par défaut sûr ; vous pourriez aussi rejeter risky, ou stocker le score et laisser passer — c'est une décision de politique, et le guide d'inscription serverless explique jusqu'où être strict au moment de la collecte.

Assembler les couches pour valider une adresse e-mail dans Flask#

L'endroit idiomatique pour ajouter un contrôle par champ dans Flask-WTF est une méthode en ligne validate_<nomduchamp> sur le formulaire. WTForms ajoute cette méthode à la chaîne de validateurs du champ et l'exécute après les validateurs de la liste : au moment où elle se déclenche, Email(check_deliverability=True) a donc déjà réglé la syntaxe et la résolution MX. Protégez-vous avec field.errors pour que l'aller-retour vers l'API n'ait lieu que pour les adresses ayant franchi les deux couches moins coûteuses :

from flask_wtf import FlaskForm
from wtforms import StringField
from wtforms.validators import DataRequired, Email, ValidationError

class SignupForm(FlaskForm):
    email = StringField("Email", validators=[
        DataRequired(),
        Email(check_deliverability=True),   # layers 1 + 2
    ])

    def validate_email(self, field):        # layer 3 — runs after the list above
        if field.errors:                    # syntax or MX already failed
            return                          # don't spend an API credit
        if verify(field.data)["status"] == "undeliverable":
            raise ValidationError("We couldn't confirm a mailbox at this address.")

Cette protection field.errors est toute l'astuce. Une adresse mal formée ou un domaine mort enregistre une erreur dans la chaîne avant que la méthode en ligne ne s'exécute : la protection empêche donc l'appel verify sur les adresses qui n'avaient aucune chance — une validation dans le mauvais ordre, ou sauter la protection, dépense un crédit à chaque faute de frappe. Dans une vue, validate_on_submit exécute tout le pipeline avant que le corps de votre gestionnaire ne s'exécute :

@app.route("/signup", methods=["POST"])
def signup():
    form = SignupForm()
    if form.validate_on_submit():
        # every layer passed — safe to persist
        create_account(form.email.data)
        return redirect(url_for("welcome"))
    return render_template("signup.html", form=form), 400

Une chose à décider d'emblée : ce qui se passe quand l'appel API lui-même échoue. Un délai d'expiration réseau ou une exception requests à l'intérieur de validate_email ne devrait pas provoquer un 500 sur la requête ni infliger à un vrai client une erreur qu'il ne peut pas corriger. Interceptez requests.RequestException séparément de ValidationError, et traitez un service injoignable comme unknown plutôt que undeliverable — laissez passer l'inscription et revérifiez l'adresse plus tard, au lieu de bloquer l'enregistrement sur une panne transitoire. Les couches syntaxe et MX ont déjà tourné : vous n'assouplissez donc que la couche qui dépend du réseau.

Toutes les applications Flask n'utilisent pas WTForms. Pour une API JSON, les trois mêmes couches se lisent directement dans une route, renvoyant un 400 sur une mauvaise adresse au lieu de lever ValidationError :

from flask import Flask, request, jsonify
from email_validator import validate_email, EmailNotValidError

app = Flask(__name__)

@app.post("/api/signup")
def api_signup():
    email = (request.get_json(silent=True) or {}).get("email", "")
    try:
        info = validate_email(email, check_deliverability=True)  # layers 1 + 2
    except EmailNotValidError as exc:
        return jsonify(error=str(exc)), 400

    if verify(info.normalized)["status"] == "undeliverable":     # layer 3
        return jsonify(error="Email address appears undeliverable."), 400

    # ... create the account
    return jsonify(ok=True), 201

info.normalized est l'adresse canonisée que email-validator renvoie, et c'est elle que vous devriez stocker. C'est la même structure à trois couches que vous retrouverez dans les versions Node.js et Python de ce guide — c'est le découpage en couches, pas le framework, qui fait fonctionner la validation. Au moment de choisir quel service de vérification alimente la troisième couche, la comparaison d'API aligne les options.

Foire aux questions#

Le validateur Email de WTForms suffit-il pour valider une adresse e-mail ?#

Pour la syntaxe, oui — Email() délègue au paquet email-validator et constitue un bien meilleur contrôle de première couche que n'importe quelle expression régulière artisanale. Mais par défaut, il valide la forme, pas la délivrabilité. Ajoutez check_deliverability=True et il effectue aussi une résolution MX en direct, qui écarte les domaines morts ; même dans ce cas, il ne contacte jamais la boîte aux lettres, si bien qu'un succès signifie « le domaine accepte le courrier », pas « cette adresse existe ». Associez-le à un contrôle SMTP de la boîte aux lettres avant de faire confiance à l'adresse.

Comment ajouter un validateur d'e-mail personnalisé dans Flask-WTF ?#

Deux options idiomatiques. Pour un contrôle ponctuel, ajoutez au formulaire une méthode en ligne nommée validate_<nomduchamp> — par exemple validate_email(self, field) — et levez wtforms.validators.ValidationError lorsque la valeur échoue ; WTForms l'exécute après les validateurs listés sur le champ. Pour une règle réutilisable, écrivez un appelable qui prend (form, field) et lève la même erreur, puis passez-le dans la liste validators du champ. C'est dans l'un ou l'autre que doit se placer l'appel verify de Qualisend.

Puis-je vérifier l'existence d'une boîte aux lettres dans Flask sans API ?#

En partie. email-validator avec check_deliverability=True (ou dnspython directement) confirme que le domaine accepte le courrier, ce qui écarte gratuitement les domaines morts et ne nécessite rien de plus qu'une petite dépendance. Confirmer la boîte aux lettres suppose un dialogue SMTP que vous pouvez tenter avec smtplib mais ne devriez pas exécuter depuis votre serveur applicatif — le port 25 est largement bloqué et le résultat dépend de la réputation de votre IP. C'est la couche qu'un service de vérification existe précisément pour prendre en charge.

Faut-il valider les e-mails à l'inscription ou lors du nettoyage d'une liste ?#

Les deux, à des profondeurs différentes. Exécutez les couches syntaxe et MX de façon synchrone dans le formulaire ou la route — elles sont assez rapides pour bloquer la requête et offrir un retour immédiat — et agissez aussi sur le verdict de l'API à cet endroit. Réservez la vérification par lots, plus lourde, au nettoyage de listes et au travail de back-office, où la latence importe peu et où vous pouvez traiter les adresses en masse plutôt qu'un envoi de formulaire à la fois.


Prêt à ajouter la couche SMTP ? Déposez une adresse approuvée par Email() dans le vérificateur d'e-mails gratuit pour voir une chaîne jugée valide par WTForms revenir undeliverable, puis câblez le même verdict dans votre formulaire à l'aide de la référence de l'API — exemples à copier-coller inclus.

Your reputation, protected.

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

Get started