Anleitung · Python

E-Mail-Validierung in Python: Format, DNS und Postfach prüfen

Von der einzeiligen Formatprüfung bis zur geprüften CSV-Datei: eine getestete Regex, das Paket email-validator, eine MX-Abfrage mit dnspython, eine Postfachprüfung in einem FastAPI-Endpunkt und ein Bulk-Job für ganze Listen.

Aktualisiert am 29. Oktober 2026 · 11 Min. Lesezeit

E-Mail-Adresse in Python mit dem Paket email-validator validieren

E-Mail-Validierung in Python meint meist eine von drei Prüfungen: Ist die Adresse korrekt aufgebaut, empfängt ihre Domain E-Mails, und existiert das Postfach? Für die erste hat die Standardbibliothek keinen fertigen Validator, und die beiden anderen kann keine Formatprüfung beantworten. Diese Anleitung baut alle drei in der Reihenfolge, in der sie laufen, mit Code, den wir unter Python 3.14 gegen einen lokalen DNS-Server und eine lokale Nachbildung der API ausgeführt haben.

1. Das Format mit einer Regex prüfen

Ohne zusätzliche Pakete nutzen Sie das getestete Muster aus unserem englischen Leitfaden zur E-Mail-Regex mit re.fullmatch:

# email_regex.py
import re

EMAIL_REGEX = re.compile(
    r"(?=.{1,254}\Z)(?=.{1,64}@)"
    r"[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[A-Za-z0-9!#$%&'*+/=?^_`{|}~-]+)*"
    r"@(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+"
    r"(?:[A-Za-z]{2,63}|xn--[A-Za-z0-9-]{2,59})"
)


def is_valid_email(value: str) -> bool:
    return EMAIL_REGEX.fullmatch(value) is not None

Die Lookaheads prüfen die Längengrenzen (254 Zeichen insgesamt, 64 vor dem @). Der Rest erlaubt Plus-Tags und Apostrophe und lehnt Punkte am Rand des lokalen Teils, doppelte Punkte und Domains ohne Top-Level-Domain ab. Verwenden Sie fullmatch(): Mit re.match() und einem Muster, das auf $ endet, akzeptiert Python "jane@example.com\n", weil $ auch vor einem abschließenden Zeilenumbruch passt.

Die Regex kennt internationale Domains nur in Punycode (xn--…) und entfernt keine Leerzeichen. Beides erledigt die Bibliothek im nächsten Schritt.

2. Mit email-validator prüfen und normalisieren

Das Paket email-validator prüft die Syntax Regel für Regel, nennt einen verständlichen Grund, wenn eine Adresse durchfällt, und liefert eine normalisierte Form:

# normalize.py: pip install email-validator
from email_validator import EmailNotValidError, validate_email


def normalize_email(value: str) -> str:
    """Liefert die Adresse in ASCII-Form oder wirft EmailNotValidError mit verständlicher Meldung."""
    info = validate_email(
        value.strip(),
        check_deliverability=False,  # hier keine DNS-Abfragen
        allow_smtputf8=False,  # lokale Teile mit Nicht-ASCII-Zeichen wie josé@ ablehnen
        strict=True,  # Grenze von 64 Zeichen vor dem @ prüfen
    )
    return info.ascii_email  # internationale Domains in Punycode

Die drei Schlüsselwortargumente sind wichtig. allow_smtputf8=False lehnt Adressen wie josé@example.com ab, die nur Mailserver mit SMTPUTF8-Unterstützung erreichen. strict=True schaltet die Grenze von 64 Zeichen für den lokalen Teil ein, die Version 2.3.0 standardmäßig nicht prüft. Und ascii_email liefert die Domain in Punycode, normalized dagegen in Unicode.

Jede Adresse unten lief durch die Regex, durch normalize_email und durch EmailStr von Pydantic, das email-validator mit den Standardeinstellungen aufruft. ✓ heißt, die Prüfung nimmt die Adresse an, ✗, dass sie sie ablehnt.

AdresseSoll bestehen?Regexnormalize_emailEmailStr
jane@example.comJa✓✓✓
jane.doe+news@example.comJa✓✓✓
o'brien@example.ieJa✓✓✓
Anna@Example.COMJa✓✓ Anna@example.com✓ Anna@example.com
jane@xn--bcher-kva.exampleJa✓✓✓ jane@bücher.example
jane@bücher.exampleNach IDN-Umwandlung✗✓ jane@xn--bcher-kva.example✓
jane.@example.comNein✗✗✗
jane..doe@example.comNein✗✗✗
jane@exampleNein✗✗✗
jane@example.cNein✗✓✓
jane@-example.comNein✗✗✗
jane@exa_mple.comNein✗✗✗
jane doe@example.comNein✗✗✗
"jane doe"@example.comNein (laut RFC gültig)✗✗✗
jane@[192.0.2.10]Nein (laut RFC gültig)✗✗✗
josé@example.comNein (braucht SMTPUTF8)✗✗✓
aaa…a@example.com (65 a)Nein✗✗✓
jane@example.com mit Leerzeichen am RandNach dem Trimmen✗✓✓
jane@mail.localNein (reservierter Name)✓✗✗

Getestet mit Python 3.14, email-validator 2.3.0 und Pydantic 2.14. Die Fehlermeldungen sind für Menschen geschrieben, allerdings auf Englisch: Für jane..doe@example.com lautet die Meldung “An email address cannot have two periods in a row.” email-validator lehnt außerdem reservierte Domains wie .local, .test und localhost ab, die die Regex durchlässt. Beachten Sie, was EmailStr mit der Domain macht: Es gibt jane@xn--bcher-kva.example als jane@bücher.example zurück. Wandeln Sie die Adresse also zurück, bevor Sie sie an eine API oder einen Mailserver schicken.

3. Die MX-Einträge der Domain prüfen

Eine korrekt aufgebaute Adresse auf einer Domain ohne Mailserver kommt trotzdem zurück. Auch das prüft email-validator: validate_email(email, check_deliverability=True) fragt die MX-Einträge der Domain ab. Gegen unseren lokalen Test-DNS-Server lehnte es einen Null-MX („does not accept email“) und eine nicht existierende Domain ab, griff bei einer Domain ohne MX-Eintrag auf den A- oder AAAA-Eintrag zurück und nahm die Adresse an, wenn der DNS-Server einen Fehler meldete oder nicht antwortete. Es prüft außerdem, ob eine solche Ersatzadresse öffentlich ist: Ein A-Eintrag aus einem Dokumentationsbereich wie 192.0.2.0/24 wurde abgelehnt.

Wenn Sie die Antwort als Wert statt als Ausnahme brauchen und „unbekannt“ von „nein“ unterscheiden wollen, liefert dnspython die Bausteine:

# mx.py: pip install dnspython
import dns.exception
import dns.resolver

resolver = dns.resolver.Resolver()
resolver.lifetime = 3.0  # Sekunden pro Abfrage, Wiederholungen eingeschlossen


def accepts_mail(domain: str) -> bool | None:
    """True: die Domain nimmt E-Mails an. False: nicht. None: DNS gab keine klare Antwort."""
    try:
        answer = resolver.resolve(domain, "MX")
        hosts = [r.exchange.to_text() for r in answer]
        return hosts != ["."]  # ein einzelnes "." ist ein Null-MX (RFC 7505): keine E-Mails
    except dns.resolver.NXDOMAIN:
        return False  # die Domain existiert nicht
    except dns.resolver.NoAnswer:
        pass  # kein MX-Eintrag: unten auf A/AAAA zurückgreifen
    except dns.exception.DNSException:
        return None  # Zeitüberschreitung, SERVFAIL: unbekannt, nicht ablehnen

    # Kein MX-Eintrag: Absender stellen an den A- oder AAAA-Eintrag der Domain zu (impliziter MX).
    unknown = False
    for rdtype in ("A", "AAAA"):
        try:
            resolver.resolve(domain, rdtype)
            return True
        except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
            pass
        except dns.exception.DNSException:
            unknown = True
    return None if unknown else False

Mit dnspython 2.9.1 gegen den Test-Server:

Domain eingerichtet mitaccepts_mail
Zwei MX-EinträgenTrue
Null-MX (0 .)False
Nur A-EintragTrue
Nur AAAA-EintragTrue
Domain existiert nicht (NXDOMAIN)False
SERVFAILNone
Keine AntwortNone, nach etwa 3 Sekunden

NoNameservers, das dnspython wirft, wenn alle Server versagen, und LifetimeTimeout sind Unterklassen von DNSException und landen deshalb bei None. Behandeln Sie None als „unbekannt“ und lehnen Sie die Adresse deswegen nicht ab. Ein einzelner MX-Eintrag mit dem Host . ist die Art, wie eine Domain sagt, dass sie keine E-Mails annimmt (RFC 7505); example.com veröffentlicht so einen. Mit dem MX-Lookup prüfen Sie eine Domain von Hand.

4. Prüfen, ob das Postfach existiert

Eine Domain mit funktionierenden MX-Einträgen nimmt E-Mails an, aber das heißt nicht, dass anna@example.org existiert. Das weiß nur der empfangende Mailserver, und ihn mit smtplib von Ihrem eigenen Server aus zu fragen, ist unzuverlässig: Ausgehender Port 25 ist bei den meisten Cloud-Anbietern gesperrt, Catch-all-Domains nehmen jede Adresse an, und wiederholte Anfragen können Ihre IP sperren lassen. Die E-Mail-Validierungs-API führt Syntax-, DNS-, MX- und Postfachprüfung in einer Anfrage aus, ohne eine E-Mail zu senden:

# check_mailbox.py: pip install requests
import os

import requests

API_URL = "https://api.emailvalidation.io/v1/info"


def check_mailbox(email: str) -> dict:
    """Liefert das Ergebnis der API. Wirft requests.RequestException bei Fehlern und Zeitüberschreitung."""
    response = requests.get(
        API_URL,
        params={"email": email, "timeout": 8},  # die API antwortet innerhalb von 8 Sekunden ...
        headers={"apikey": os.environ["EMAILVALIDATION_KEY"]},
        timeout=10,  # ... und der Client wartet etwas länger
    )
    response.raise_for_status()  # 401 falscher Schlüssel, 429 Kontingent oder Parallelitätslimit
    return response.json()

Der Schlüssel kommt aus der Umgebung und steht im Header apikey, er bleibt also aus Ihrem Code und aus URLs heraus. requests kodiert das + in jane+news@example.org als %2B, damit der Tag unverändert ankommt. Der API-Parameter timeout (3 bis 60 Sekunden, Standard 30) begrenzt, wie lange die API auf einen langsamen Mailserver wartet; laut Dokumentation (Englisch) sollte der Client ein paar Sekunden länger warten. Übergeben Sie requests immer ein timeout: Ohne kann eine Anfrage ewig warten. In unserem Test führte ein falscher Schlüssel zu HTTPError mit 401, ein aufgebrauchtes Kontingent zu HTTPError mit 429 und eine Antwort nach mehr als 10 Sekunden zu ReadTimeout; alle drei sind Unterklassen von requests.RequestException.

Die Antwort hat 21 Felder. Diese brauchen Sie für Entscheidungen:

statusBeispiele für sub_statusrecommended_actionWas tun
validnull, role_based, possible_typosend, send_with_cautionAnnehmen; did_you_mean anzeigen, wenn es nicht leer ist.
invalidmailbox_not_found, no_dns_entriessuppressAblehnen.
catch_allaccept_allsend_with_cautionAnnehmen oder markieren: Die Domain nimmt jede Adresse an.
riskymailbox_fullretry_laterAnnehmen; das Postfach ist gerade voll.
do_not_maildisposablesuppressNach einer dauerhaften Adresse fragen.
unknowntimeout_exceeded, greylistedretry_laterAnnehmen und später erneut prüfen.

Die älteren Felder state (deliverable, undeliverable, unknown) und reason liefern dasselbe Urteil weniger detailliert; die Seite Result Statuses (Englisch) ordnet sie zu. Warum sich Catch-all-Domains nicht Postfach für Postfach prüfen lassen, erklärt Catch-all-E-Mail-Adressen.

Es gibt auch ein offizielles SDK, pip install emailvalidationio: emailvalidationio.Client(key).validate(email) sendet dieselbe Anfrage. In Version 0.2 setzt es keine Zeitbegrenzung, für ein Anmeldeformular sind die paar Zeilen mit requests oben deshalb die sicherere Wahl.

5. E-Mail-Adressen in einem FastAPI-Endpunkt validieren

In einer Webanwendung antwortet die Formatprüfung sofort, und die API-Prüfung läuft mit ihrer Zeitbegrenzung. Ist die API langsam, nicht erreichbar oder das Kontingent aufgebraucht, geht die Anmeldung trotzdem durch:

# app.py: pip install fastapi uvicorn email-validator requests
import logging

import requests
from email_validator import EmailNotValidError
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel

from check_mailbox import check_mailbox
from normalize import normalize_email

app = FastAPI()
log = logging.getLogger("signup")


class Signup(BaseModel):
    email: str


@app.post("/signup", status_code=201)
def signup(body: Signup):  # ein normales def: FastAPI führt es im Threadpool aus, blockierende Aufrufe sind also in Ordnung
    try:
        email = normalize_email(body.email)
    except EmailNotValidError as err:
        raise HTTPException(422, f"Bitte geben Sie eine gültige E-Mail-Adresse ein. {err}")

    result = None
    try:
        result = check_mailbox(email)
    except requests.RequestException as err:
        log.warning("email check skipped: %s", err)  # API ausgefallen oder langsam: Anmeldung nicht blockieren

    if result and result["status"] == "invalid":
        raise HTTPException(422, "Diese E-Mail-Adresse existiert nicht. Bitte prüfen Sie sie.")
    if result and result["status"] == "do_not_mail":
        raise HTTPException(422, "Bitte verwenden Sie eine dauerhafte E-Mail-Adresse.")

    # Konto anlegen ...
    return {"email": email, "did_you_mean": (result or {}).get("did_you_mean") or None}

email ist absichtlich ein einfacher str: Mit EmailStr würde Pydantic fehlerhafte Adressen mit seinem eigenen Fehlerformat ablehnen und josé@ sowie lokale Teile mit 65 Zeichen annehmen (siehe Tabelle oben). Wir haben den Endpunkt mit FastAPI 0.162 und dessen TestClient gegen eine lokale Nachbildung der API ausgeführt, die das dokumentierte Antwortformat liefert:

Gesendetes emailAntwort
anna@example.org201
anna@example.org mit Leerzeichen am Rand201, gespeichert als anna@example.org
jane@bücher.example.com201, an die API gesendet als jane@xn--bcher-kva.example.com
jane@example422, „Bitte geben Sie eine gültige E-Mail-Adresse ein.“ mit der englischen Begründung von email-validator
josé@example.com422, mit der Begründung von email-validator
Eine Adresse, die die API als invalid meldet422, „Diese E-Mail-Adresse existiert nicht. Bitte prüfen Sie sie.“
Eine Adresse, die die API als do_not_mail meldet422, „Bitte verwenden Sie eine dauerhafte E-Mail-Adresse.“
Eine Adresse auf einer Catch-all-Domain201
anna@gmial.com201 mit "did_you_mean": "anna@gmail.com"
Beliebige Adresse, während die API 429 antwortet201, Warnung protokolliert

In Django passt dieselbe Logik in die Methode clean_email eines Formulars, in Flask in die View-Funktion.

6. Eine CSV-Datei mit der Bulk-API prüfen

/v1/info einmal pro Zeile aufzurufen, funktioniert für kleine Listen, aber Ihr Tarif begrenzt, wie viele Prüfungen gleichzeitig laufen (5 im kostenlosen Tarif, 25 in bezahlten Tarifen), und der kostenlose Tarif erlaubt 10 Anfragen pro Minute. Eine ganze Datei schicken Sie an den Bulk-Endpunkt: Eine Anfrage lädt bis zu 100.000 Zeilen hoch, und die API liefert Ihre Spalten plus 18 Ergebnisspalten zurück.

# bulk.py: python bulk.py contacts.csv
import os
import sys
import time

import requests

API = "https://api.emailvalidation.io/v1/bulk"
HEADERS = {"apikey": os.environ["EMAILVALIDATION_KEY"]}

path = sys.argv[1]

# 1. CSV-Datei hochladen; die API erkennt die Spalte mit den Adressen.
with open(path, "rb") as f:
    response = requests.post(API, headers=HEADERS, files={"file": (os.path.basename(path), f)}, timeout=120)
response.raise_for_status()
job = response.json()
print(f"job {job['job_id']}: {job['counts']['unique']} unique addresses, {job['quota']['charged']} charged")

# 2. Etwa einmal pro Minute abfragen, bis der Job fertig ist (große Listen können Stunden dauern).
while job["status"] in ("queued", "processing"):
    time.sleep(60)
    response = requests.get(f"{API}/{job['job_id']}", headers=HEADERS, timeout=30)
    response.raise_for_status()
    job = response.json()
    print(f"{job['status']} {job['progress']}%")
if job["status"] == "failed":
    sys.exit(f"job failed: {job['error']}")

# 3. Ergebnisse herunterladen: Ihre Spalten plus die Spalten emailvalidation_*.
response = requests.get(f"{API}/{job['job_id']}/results", headers=HEADERS, params={"format": "csv"}, timeout=120)
response.raise_for_status()
out = path.removesuffix(".csv") + ".verified.csv"
with open(out, "wb") as f:
    f.write(response.content)
print("saved", out)

# 4. Job und gespeicherte Liste löschen (sonst nach 30 Tagen automatisch).
requests.delete(f"{API}/{job['job_id']}", headers=HEADERS, timeout=30).raise_for_status()

Duplikate und Zeilen ohne gültige Adresse sind kostenlos; berechnet werden beim Hochladen nur eindeutige, gültige Adressen. Die Ergebnisdatei behält Ihre Zeilen in ihrer Reihenfolge und ergänzt Spalten wie emailvalidation_state, emailvalidation_status und emailvalidation_recommended_action; filtern Sie nach der letzten, um Ihre Versandliste zu erhalten. Statt abzufragen, können Sie eine webhook_url übergeben und erhalten eine signierte Anfrage, sobald der Job fertig ist. Die Details stehen in der Bulk-Dokumentation (Englisch). Wir haben das Skript gegen eine lokale Nachbildung der vier Bulk-Endpunkte mit einer Datei aus fünf Zeilen ausgeführt, darunter ein Duplikat und eine fehlerhafte Adresse.

Ohne Code geht das auch im Browser: Der Bulk-E-Mail-Validator prüft eine kurze Liste.

Empfohlener Aufbau

  1. Bei der Eingabe: normalize_email für das Format, mit verständlichen Fehlermeldungen.
  2. Optional accepts_mail, wenn Sie tote Domains ohne API-Aufruf abfangen wollen.
  3. Beim Absenden: check_mailbox. Blockieren Sie invalid und do_not_mail, nehmen Sie den Rest an, und nehmen Sie alles an, wenn die API nicht erreichbar ist.
  4. Für Listen: bulk.py statt einer Schleife über /v1/info.

Der kostenlose Tarif enthält 100 Prüfungen im Monat, und nur eindeutige Ergebnisse (zustellbar oder nicht zustellbar) zählen dagegen; die bezahlten Tarife finden Sie unter Preise der E-Mail-Validierungs-API. Dieselben Prüfungen in anderen Sprachen: E-Mail-Validierung in JavaScript und E-Mail-Validierung in PHP.

Quellen

Geprüft am 29. Oktober 2026: Python-Dokumentation, re.fullmatch; README von email-validator; Resolver-Dokumentation von dnspython; Pydantic, Netzwerktypen; Requests, Timeouts; RFC 7505 (Null-MX); API-Dokumentation von emailvalidation.io: Validierungs-Endpunkt, Bulk-Jobs, Rate Limits. Code ausgeführt mit Python 3.14, email-validator 2.3.0, dnspython 2.9.1, requests 2.34, Pydantic 2.14 und FastAPI 0.162, gegen einen lokalen DNS-Server und eine lokale Nachbildung der API.

E-Mail-Validierung in anderen Sprachen

Häufig gestellte Fragen

Wie validiere ich eine E-Mail-Adresse in Python?

Für das Format nutzen Sie das Paket email-validator (pip install email-validator) oder eine getestete Regex mit re.fullmatch. Ob die Adresse existiert, verrät keines von beiden. Für Anmeldungen und Importe prüfen Sie zusätzlich die MX-Einträge der Domain und das Postfach, etwa mit einer E-Mail-Validierungs-API.

Welche Python-Bibliothek validiert E-Mail-Adressen?

Üblich ist email-validator, und auch EmailStr von Pydantic nutzt es. Das Paket prüft die Syntax, normalisiert die Adresse und fragt mit check_deliverability=True die MX-Einträge der Domain ab. Ob das Postfach existiert, prüft es nicht.

Warum re.fullmatch statt re.match für die E-Mail-Validierung?

re.match verankert nur den Anfang, und ein Muster, das auf $ endet, passt auch vor einem abschließenden Zeilenumbruch, sodass "jane@example.com\n" durchkommt. re.fullmatch verlangt, dass der ganze String passt.

Kann ich eine E-Mail-Adresse mit smtplib prüfen?

Theoretisch können Sie eine SMTP-Verbindung öffnen und RCPT TO senden. In der Praxis ist ausgehender Port 25 bei den meisten Cloud-Anbietern gesperrt, viele Server nutzen Greylisting oder nehmen jede Adresse an (Catch-all), und wiederholte Anfragen können Ihre IP auf eine Blockliste bringen. Eine API führt diese Prüfungen mit eigener Infrastruktur aus.

Wie prüfe ich eine CSV-Datei mit E-Mail-Adressen in Python?

Laden Sie die Datei beim Bulk-Endpunkt einer E-Mail-Validierungs-API hoch, fragen Sie den Job ab, bis er abgeschlossen ist, und laden Sie die Ergebnisse als CSV herunter. Die Bulk-API von emailvalidation.io nimmt bis zu 100.000 Zeilen pro Job an und liefert Ihre Spalten plus die Ergebnisspalten.

E-Mail-Validierungs-API

E-Mail-Adressen in Ihrer Anwendung prüfen

emailvalidation.io prüft Syntax, MX-Einträge und das Postfach per SMTP, markiert Wegwerf-, Rollen- und Freemail-Adressen und liefert einen Qualitätswert, alles in einer Anfrage.

/v1/info E-Mail-Validierungs-API Zur Dokumentation

100 kostenlose Prüfungen pro Monat. Keine Kreditkarte nötig.

GET https://api.emailvalidation.io/v1/info?email=support@emailvalidation.io

{
  "email": "support@emailvalidation.io",
  "user": "support",
  "tag": "",
  "domain": "emailvalidation.io",
  "format_valid": true,
  "mx_found": true,
  "smtp_check": true,
  "catch_all": null,
  "role": true,
  "disposable": false,
  "free": false,
  "score": 0.64,
  "state": "deliverable",
  "reason": "low_quality",
  "did_you_mean": "",
  "catch_all_resolution": null,
  "bounce_probability": null,
  "status": "valid",
  "sub_status": "role_based",
  "explanation": "The mailbox exists, but it is a role address (such as info@ or support@), usually read by a team rather than one person.",
  "recommended_action": "send_with_caution"
}

Weiterlesen

Starten Sie noch heute mit unserer E-Mail-Validierung!

Kostenlos starten