email-validator
email-validator checks that a string is a usable email address and hands you back a cleaned-up version of it. You call validate_email(address) and either get a ValidatedEmail object with a normalized field you should store in your database, or an EmailNotValidError whose message is written in plain English so you can show it to the user. It also optionally does a DNS lookup to see whether the domain has an MX record that can receive mail. The library is deliberately opinionated: it rejects @localhost, domains with no dot, the obsolete (comment) syntax, and unsafe Unicode, even though some of those are technically legal per the RFCs. It is the package pydantic installs when you use EmailStr.
The default Python answer for validating an address at a form boundary, mostly because it does normalization and internationalization correctly and gives you an error message you can print. Turn check_deliverability off everywhere except account creation, and remember it proves nothing about whether the mailbox exists.
Use it if
- You have a signup form and want one call that catches typo'd addresses, gives a user-facing error string, and returns the exact form you should write to the users table
- You accept international addresses and need IDNA 2008 handling, Punycode conversion (ascii_email), Unicode NFC normalization, and a flag telling you whether delivery needs SMTPUTF8
- You want an optional cheap deliverability signal at registration time: does the domain resolve, does it have a non-null MX record, is there a reject-all SPF record
- You already use pydantic's EmailStr, since that field type is a thin wrapper over this library and configuring it means configuring email-validator
- You need a strict RFC 5322 conformance checker: this library rejects legal-but-obsolete forms on purpose, so addresses that pass the spec fail here and the author himself points people at pyIsEmail when that matters
- You call it on a login page without turning off deliverability: check_deliverability defaults to True, so every call fires DNS queries with a 15 second default timeout and your auth endpoint inherits somebody else's nameserver latency
- You expect it to tell you whether a mailbox actually exists: it never contacts an SMTP server, so a valid-looking address on a real domain can still hard bounce and you still need double opt-in
- You only need a quick shape check inside a script and do not want dnspython and idna pulled into the environment: a regex or the validators package covers that with less weight
- You are on Python 3.8 or 3.9 and plan to stay: the in-development release after 2.3.0 drops both, so you would be pinning to an old version soon
Setup reality
pip install email-validator brings in dnspython and idna, both pure Python, so there is nothing to compile. The real trap is the default: check_deliverability=True means DNS on every call, which makes tests slow and flaky offline and turns a login handler into a network call. Set email_validator.CHECK_DELIVERABILITY = False globally or pass it per call. The second trap is test data. @example.com and @localhost both fail out of the box: localhost is in SPECIAL_USE_DOMAIN_NAMES, and example.com resolves but does not accept mail, so use test_environment=True with @something.test in fixtures. Third, if you validate in a loop, build one caching_resolver() and pass it in as dns_resolver or you re-query the same domains forever.
Patterns
Validate an address and store the normalized formvalidate-at-signup
from email_validator import validate_email, EmailNotValidError
try:
info = validate_email(raw_input_address)
email = info.normalized # store THIS
except EmailNotValidError as e:
return {"error": str(e)}, 400Always persist info.normalized, not the raw input. The domain is lowercased and Unicode is NFC-normalized, so the raw string and the normalized one can differ and your later lookups will miss.
Turn off the DNS check on hot pathsskip-dns-on-login
info = validate_email(email, check_deliverability=False)
# or globally, once at startup:
import email_validator
email_validator.CHECK_DELIVERABILITY = Falsecheck_deliverability defaults to True, so every call does DNS with a 15 second default timeout. Use it on account creation only; on login it just adds latency and a new failure mode.
Reuse one resolver when validating many addressescaching-resolver
from email_validator import validate_email, caching_resolver
resolver = caching_resolver(timeout=10)
for address in addresses:
try:
validate_email(address, dns_resolver=resolver)
except Exception as e:
print(address, e)Without a shared resolver each call re-queries DNS even for the same domain. caching_resolver builds a dns.resolver.Resolver with an LRUCache; pass the same instance every time or the cache does nothing.
Allow .test addresses in fixturestest-environment
info = validate_email("user@myapp.test", test_environment=True)
# or globally in conftest.py:
import email_validator
email_validator.TEST_ENVIRONMENT = Truetest_environment=True disables DNS and permits test and *.test. Do not reach for @example.com instead: it is not special-cased here and fails the deliverability check because it accepts no mail.
Tell a typo apart from a dead domainseparate-syntax-from-dns
from email_validator import (validate_email, EmailSyntaxError,
EmailUndeliverableError)
try:
validate_email(email)
except EmailSyntaxError as e:
flash(f"That address looks wrong: {e}")
except EmailUndeliverableError as e:
flash(f"We could not reach that domain: {e}")Both subclass EmailNotValidError, which subclasses ValueError. Treat undeliverable as soft: DNS timeouts and temporary failures land here, so blocking signup on it will lock out real users.
Handle internationalized addresses and SMTPUTF8international-addresses
info = validate_email("example@\u30c4.life", check_deliverability=False)
print(info.normalized) # example@\u30c4.life
print(info.ascii_email) # example@xn--bdk.life
print(info.smtputf8) # False
# reject anything your mail stack cannot send:
validate_email(addr, allow_smtputf8=False)ascii_email is None when the local part itself is non-ASCII, because only the domain can be Punycoded. If your SMTP relay lacks SMTPUTF8, pass allow_smtputf8=False and get a syntax error up front instead of a bounce later.
Accept "My Name <me@example.org>" inputparse-display-name
info = validate_email('"My Name" <me@example.org>',
allow_display_name=True,
check_deliverability=False)
print(info.display_name) # My Name
print(info.normalized) # me@example.orgOff by default. display_name is None when there were no angle brackets and an empty string when there were brackets but no name, so test for None explicitly rather than truthiness.
Read the MX records the check foundinspect-mx-records
info = validate_email("user@gmail.com")
print(info.mx) # [(5, 'gmail-smtp-in.l.google.com'), ...]
print(info.mx_fallback_type) # None if a real MX record existsmx can be None even on success when the lookup hit a temporary problem such as a timeout. mx_fallback_type is 'A' or 'AAAA' when the domain has no MX record and delivery falls back to the address record.
Opt back into quoted local parts and domain literalsallow-obsolete-forms
info = validate_email('"weird name"@example.com',
allow_quoted_local=True,
check_deliverability=False)
info2 = validate_email("user@[192.168.1.1]",
allow_domain_literal=True)
print(info2.domain_address) # IPv4Address('192.168.1.1')Both are rejected by default on purpose. Domain literals get no deliverability check at all, and quoted local parts are normalized (unnecessary escapes and quotes stripped), so the stored value may not match the input string.
Enforce the 64 character local part limitstrict-length-check
# 2.3.0 stopped enforcing this by default; restore it if you
# are the one issuing the mailboxes:
info = validate_email(address, strict=True, check_deliverability=False)New in 2.3.0. The library dropped the local part length check because addresses longer than 64 characters exist in the wild; the overall 254 character address limit is still enforced either way.
Use it through pydantic's EmailStrpydantic-emailstr
from pydantic import BaseModel, EmailStr
class Signup(BaseModel):
email: EmailStr
# requires: pip install "pydantic[email]"EmailStr is a wrapper over this library and does syntax only, no DNS. If you want a deliverability check in a pydantic model you have to call validate_email yourself in a field validator.
Check addresses from the shellcli-spot-check
python -m email_validator test@example.org
# or pipe a list, one address per line; only invalid ones print
python -m email_validator < addresses.txtOptions map to uppercase environment variables, so CHECK_DELIVERABILITY= is not how you disable it; the tool reads the variable as a bool, and any non-empty value including "0" counts as True.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pyIsEmail | PyPI | You want closer RFC conformance and a diagnosis code per address instead of one opinionated pass or fail. |
| validators | PyPI | You just need a boolean syntax check for an email, URL, or IP inside a script and do not want DNS anywhere near it. |
| pydantic | PyPI | Your validation already lives in a model layer; EmailStr wraps this same library and gives you request-body validation for free. |
| email-normalize | PyPI | Your problem is deduplicating addresses across providers (gmail dots and plus tags) rather than deciding validity. |