Python

Validate Email Addresses in Python with email-validator

Python · Published 2026-10-27

To validate an email address in Python, install email-validator and call validate_email(address). It returns an object with a normalized form of the address, or raises EmailNotValidError with a human-readable message. By default it also checks DNS to confirm that the domain can receive mail; pass check_deliverability=False to check syntax only.

The examples below were run with email-validator 2.3 on Python 3.13. Install it with pip install email-validator; the import name uses an underscore, email_validator.

Why not a regular expression?

The full address grammar in RFC 5322 allows quoted strings, comments and other forms that no mail provider accepts in practice, while internationalized addresses (RFC 6531) allow non-ASCII characters that most hand-written regexes reject. A regex is either too strict or too lax, and it gives users no useful error message. email-validator targets the addresses that real mail systems handle, explains what is wrong, and returns a normalized form you can store.

The standard library's email.utils.parseaddr is not a validator: it splits a header like "Ada <ada@example.com>" into name and address, and accepts almost anything.

Basic validation

from email_validator import validate_email, EmailNotValidError

def check(address):
    try:
        info = validate_email(address, check_deliverability=False)
        return info.normalized
    except EmailNotValidError as e:
        return f"invalid: {e}"

for a in ["Ada.Lovelace@Example.COM", "ada@@example.com", "ada@example",
          "ada lovelace@example.com", ".ada@example.com", "ada@exa_mple.com"]:
    print(f"{a!r:28} -> {check(a)}")
'Ada.Lovelace@Example.COM'   -> Ada.Lovelace@example.com
'ada@@example.com'           -> invalid: The part after the @-sign contains invalid characters: '@'.
'ada@example'                -> invalid: The part after the @-sign is not valid. It should have a period.
'ada lovelace@example.com'   -> invalid: The email address contains invalid characters before the @-sign: SPACE.
'.ada@example.com'           -> invalid: An email address cannot start with a period.
'ada@exa_mple.com'           -> invalid: The part after the @-sign contains invalid characters: '_'.

Two things stand out. The error messages are written for end users, so you can show str(e) next to a form field. And the valid address comes back with its domain lowercased, while the local part (before the @) keeps its case: domains are case-insensitive, local parts technically are not, even though most providers ignore case.

Always store the normalized form

Use info.normalized as the value you save and compare, not the raw input. Otherwise ada@Example.com and ada@example.com become two accounts. Normalization also applies Unicode normalization (NFC) and converts internationalized domain names to their Unicode form.

The returned ValidatedEmail object has more attributes:

info = validate_email("José.Silva@Bücher.Example", check_deliverability=False)
print(info.normalized)
print(info.local_part, info.domain)
print(info.ascii_domain)
print(info.ascii_email)
print(info.smtputf8)
José.Silva@bücher.example
José.Silva bücher.example
xn--bcher-kva.example
None
True

ascii_domain is the Punycode ("IDNA") form of the domain, which is what DNS actually uses. ascii_email is None here because the local part contains "é": such an address can only be delivered by mail servers that support the SMTPUTF8 extension, which smtputf8 = True signals. For a pure-ASCII address, ascii_email equals the normalized address.

Internationalized addresses: accept or reject?

Addresses like 用户@例子.广告 are valid and accepted by default. If your mail pipeline cannot send to non-ASCII local parts, reject them up front with allow_smtputf8=False:

try:
    validate_email("José@example.com", allow_smtputf8=False, check_deliverability=False)
except EmailNotValidError as e:
    print(e)
Internationalized characters before the @-sign are not supported: 'é'.

Internationalized domains with an ASCII local part are still accepted, because they can always be sent in their ASCII form.

Deliverability: the DNS check

With the default check_deliverability=True, the library looks up the domain's MX records (falling back to A/AAAA records) and rejects domains that do not exist or that publish a "null MX" saying they accept no mail. The exception is then an EmailUndeliverableError, a subclass of EmailNotValidError.

from email_validator import EmailUndeliverableError

try:
    validate_email("x@qwzxv-not-a-real-domain-81723.com")
except EmailUndeliverableError as e:
    print("undeliverable:", e)
undeliverable: The domain name qwzxv-not-a-real-domain-81723.com does not exist.

Practical advice on the DNS check:

Special-use and reserved domains such as .invalid, .local or localhost are rejected even with check_deliverability=False, because they can never receive Internet mail. In automated tests, either use real-looking domains with the DNS check turned off, or set email_validator.TEST_ENVIRONMENT = True, which allows the reserved test domain and skips DNS.

Options for unusual addresses

A few valid but rare forms are rejected by default because most systems cannot handle them. Enable them only if you need them:

OptionAllows
allow_quoted_local=True"ada lovelace"@example.com
allow_domain_literal=Trueada@[192.0.2.1]
allow_display_name=TrueAda Lovelace <ada@example.com>
allow_empty_local=True@example.com (for patterns, not mailboxes)

Each keyword has a module-level default (for example email_validator.ALLOW_QUOTED_LOCAL) if you want to change it for the whole application.

Validating a list of addresses

For a CSV of contacts, split valid and invalid entries and keep the reason, so someone can fix the data instead of silently losing rows:

from email_validator import validate_email, EmailNotValidError

raw = ["Ada@Example.com", "grace@@example.com", " alan@example.org "]
valid, rejected = [], []
for addr in raw:
    try:
        valid.append(validate_email(addr.strip(), check_deliverability=False).normalized)
    except EmailNotValidError as e:
        rejected.append((addr, str(e)))
print(valid)
print(rejected)
['Ada@example.com', 'alan@example.org']
[('grace@@example.com', "The part after the @-sign contains invalid characters: '@'.")]

Strip whitespace yourself: the library treats a leading or trailing space as an invalid character rather than guessing what you meant. If you parse the addresses out of longer text first, the guide on splitting strings into lists covers the separators you will meet.

Common pitfalls

Summary

validate_email(address) checks syntax against the rules real mail systems follow, optionally checks the domain in DNS, and returns a normalized address with user-friendly errors on failure. Store .normalized, choose whether to accept internationalized local parts with allow_smtputf8, run the DNS check at sign-up time, and confirm ownership with an email.

Sources