Eine E-Mail-Adresse in Python zu validieren bedeutet drei separate Prüfungen, und diejenige, die die meisten Tutorials zeigen – eine Regex – ist die am wenigsten nützliche davon. Echte Validierung ist geschichtet: Syntax, dann ein DNS-Lookup, dann eine SMTP-Postfachprüfung. Pythons Standardbibliothek und eine kleine Abhängigkeit decken die ersten beiden ab; die dritte ist ein Netzwerkproblem, das man besser delegiert. Diese Anleitung baut jede Schicht mit funktionierendem Code auf.
Die kurze Antwort#
Verwenden Sie eine permissive Regex (oder email.utils.parseaddr) für die Syntax,
dnspython für den MX-Lookup und eine Verifizierungs-API für die SMTP-Postfachprüfung –
die günstigste zuerst, wobei Sie abbrechen, sobald eine davon entscheidend ist.
Greifen Sie nicht zu smtplib, um Postfächer aus Ihrer App heraus zu prüfen: Der
ausgehende Port 25 ist auf den meisten Hostern blockiert, und das Ergebnis hängt von
der Reputation der sendenden IP und von
Greylisting ab, das Sie nicht selbst nachbauen wollen.
Schicht 1: Syntax#
Halten Sie es permissiv – fangen Sie Tippfehler ab, bauen Sie nicht RFC 5322 nach (was ohnehin nicht helfen würde):
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
Das email.utils.parseaddr der Standardbibliothek parst zwar eine Adresse, aber es
ist bewusst nachsichtig – parseaddr("nonsense") liefert ('', 'nonsense') zurück,
ohne einen Fehler auszulösen – es ist also ein Parser, kein Validator. Eine kurze Regex
ist für ein Ja/Nein-Syntax-Gate klarer.
Schicht 2: Kann die Domain überhaupt Mail empfangen?#
Eine Domain ohne MX-Records kann für niemanden Mail annehmen. dnspython löst sie in
ein paar Zeilen auf:
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
Dieser eine Lookup eliminiert tote Domains und erfundene TLDs ganz nebenbei. Wenn Sie
Domains berücksichtigen wollen, die Mail auf einem A-Record ohne MX annehmen, fallen Sie
auf das Auflösen von A/AAAA zurück, wenn die MX-Menge leer ist – aber eine MX-Prüfung
deckt die überwältigende Mehrheit der echten Adressen ab.
Schicht 3: Existiert das Postfach tatsächlich?#
Die Schichten 1 und 2 können eine Adresse nur ausschließen. Ein Postfach zu bestätigen
bedeutet das SMTP-Zustellgespräch – und während smtplib ein solches eröffnen kann, ist
es eine schlechte Idee, es aus Ihrer Anwendung heraus auszuführen: Port 25 ist weithin
blockiert, die Antwort hängt von der Reputation der IP ab, von der aus Sie sich verbinden,
und empfangende Server wenden Greylisting und Ratenbegrenzung auf Fremde an. Delegieren
Sie diese Schicht.
Qualisends POST /verify führt die vollständige Pipeline aus. Die lokalen
Prüfungen kehren sofort zurück, während die SMTP-Prüfung in die Warteschlange gestellt wird:
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()
Für die Live-Validierung bei der Anmeldung reicht das sofortige result meist aus, um zu
handeln. Wenn Sie das SMTP-bestätigte Urteil brauchen, prüfen Sie zuerst result["smtp"]:
cached oder skipped bedeutet, dass das Ergebnis bereits endgültig ist; nur bei pending
müssen Sie den Job abfragen. Tun Sie das in einem Hintergrund-Task, nicht innerhalb der
Anfrage:
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
Eine Liste im Stapel verifizieren#
Lassen Sie verify() nicht Adresse für Adresse über eine CSV laufen – das ist ein
HTTP-Roundtrip und ein Job pro Zeile. Schicken Sie die gesamte Liste in einer einzigen
Anfrage an /verify/bulk, erhalten Sie eine job_id zurück und fragen Sie einmal für den
gesamten Stapel ab. Doppelte Zeilen werden entfernt (und erstattet), und die Ergebnisse
kommen in Seiten zu höchstens 1.000 Zeilen. Blättern Sie also durch die Seiten und ordnen
Sie jede Zeile über ihre email zu:
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
Ein Stapel von Tausenden kann Minuten dauern, weil jede SMTP-Prüfung ein separates
Netzwerkgespräch ist. Weiten Sie also tries/delay für größere Listen aus, statt den
Poll-Endpunkt zu überlasten. Führen Sie zuerst die beiden lokalen Schichten aus, um
offensichtlichen Müll auszusortieren, bevor Sie Credits ausgeben – filtern Sie mit
is_valid_syntax und has_mail_route und senden Sie dann nur die Überlebenden.
Hinweise zu Async und pydantic#
requests ist blockierend. Umschließen Sie den Aufruf in einer asyncio-App daher mit
asyncio.to_thread(verify, email) oder wechseln Sie zu httpx.AsyncClient, um die
Verify-Aufrufe nebenläufig abzufeuern; dnspython bringt aus demselben Grund einen
asynchronen Resolver (dns.asyncresolver) mit. Wenn Sie bereits pydantic verwenden,
ist dessen EmailStr-Typ ein sauberer Ersatz für Schicht 1 – er validiert die Syntax,
wenn ein Modell erstellt wird –, aber dort endet es: Er berührt weder DNS noch das
Postfach, halten Sie die MX-Prüfung und die Verifizierungs-API also nachgelagert.
Die Schichten zusammensetzen#
Die günstigste zuerst, aufhören, sobald Sie eine Antwort haben:
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
Schlägt der API-Aufruf selbst fehl (ein Timeout, 402 wegen fehlender Credits, 429
wegen Ratenbegrenzung oder ein 5xx), lässt die Funktion die Adresse mit unknown
durch: Lassen Sie die Anmeldung zu und verifizieren Sie die Adresse später erneut.
Die beiden lokalen Schichten kosten nichts und fangen den meisten Müll sofort ab; die API läuft nur auf Adressen, die den Roundtrip wert sind. Siehe wie E-Mail-Verifizierung funktioniert dafür, warum die Stufen in dieser Reihenfolge stehen.
Häufig gestellte Fragen
01Kann ich eine E-Mail in Python nur mit der Standardbibliothek verifizieren?
Bis zu einem gewissen Punkt. re übernimmt die Syntax und mit socket können Sie einen DNS-Versuch unternehmen – aber es gibt keinen eingebauten MX-Resolver (daher dnspython), und um ein Postfach zu bestätigen, braucht es eine SMTP-Prüfung, die Sie nicht aus Ihrer App heraus ausführen sollten. Die Standardbibliothek liefert Ihnen Syntax und eine grobe Domain-Prüfung; die Postfach-Schicht braucht dnspython plus einen Verifizierungsdienst.
02Warum nicht smtplib verwenden, um zu prüfen, ob ein Postfach existiert?
smtplib kann das SMTP-Gespräch eröffnen, aber es von Ihrem Anwendungsserver aus auszuführen, ist unzuverlässig und riskant: Die meisten Hoster blockieren den ausgehenden Port 25, die Antwort hängt von der Reputation Ihrer sendenden IP ab, und Prüfungen in jeglichem Umfang führen dazu, dass Sie ratenbegrenzt oder auf Sperrlisten gesetzt werden. Eine Verifizierungs-API führt die Prüfung von einer dafür gebauten, reputationsverwalteten Infrastruktur aus durch.
03Ist dnspython die richtige Bibliothek für MX-Lookups?
Ja – es ist das standardmäßige, gut gepflegte DNS-Toolkit für Python und löst MX-Records sauber auf. Installieren Sie es mit pip install dnspython und fangen Sie NXDOMAIN, NoAnswer und NoNameservers ab, um eine fehlende Mail-Route als nicht zustellbar zu behandeln.
04Validiert das EmailStr von pydantic eine E-Mail-Adresse vollständig?
Nein – EmailStr (basierend auf dem Paket email-validator) prüft nur, dass eine Zeichenkette beim Erstellen Ihres Modells syntaktisch eine gültige Adresse ist. Es bestätigt nicht, dass die Domain MX-Records hat oder dass das Postfach existiert, es entspricht also genau Schicht 1. Verwenden Sie es als Syntax-Gate in Ihren pydantic-Modellen und führen Sie dann den DNS-Lookup und die Verifizierungs-API auf den Adressen aus, die durchkommen. Es normalisiert die Adresse außerdem (schreibt die Domain klein), was praktisch ist, bevor Sie eine Liste deduplizieren.
Bereit, die SMTP-Schicht hinzuzufügen? Der kostenlose Tarif enthält 100 Credits,
die die vollständige Pipeline ausführen, und die API-Referenz enthält die
kompletten /verify- und /jobs-Endpunkte mit Copy-and-paste-Beispielen in sieben Sprachen.



