Validate Email Addresses in Python with email-validator
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:
- Use it at sign-up, not on every login. It costs a network round trip. Validate syntax only when you merely read an address back from your own database.
- Reuse a resolver when checking many addresses.
resolver = caching_resolver(timeout=10), thenvalidate_email(addr, dns_resolver=resolver), caches lookups for repeated domains. - It does not prove the mailbox exists. A domain with MX records can still bounce. The only real proof is a confirmation email with a link.
- Timeouts are not treated as failures. If DNS does not answer in time, the address is accepted, so a slow resolver does not block sign-ups.
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:
| Option | Allows |
|---|---|
allow_quoted_local=True | "ada lovelace"@example.com |
allow_domain_literal=True | ada@[192.0.2.1] |
allow_display_name=True | Ada 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
- Catching the wrong exception. Catch
EmailNotValidError; it covers both syntax and deliverability failures. - Storing the raw input. Save
info.normalizedto avoid duplicates. - Lowercasing the whole address yourself. Let the library normalize the domain and leave the local part alone.
- DNS checks in unit tests. They make tests slow and flaky; disable them or use the test environment flag.
- Treating validation as verification. Only a confirmation email proves that someone controls the mailbox.
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
- IETF, RFC 5322: Internet Message Format.
- IETF, RFC 6531: SMTP Extension for Internationalized Email.
- Python documentation, email.utils: Miscellaneous utilities.
- email-validator project README (Joshua Tauberer), on GitHub and PyPI.
PythonSplit a string into a list
Split strings into lists in Python: str.split with and without sep, maxsplit, rsplit, splitlines, re.split for multiple delimiters, list(), csv and shlex.
PythonCould not convert string to float
Why float() raises "could not convert string to float": empty strings, commas, currency symbols, N/A values, and how to fix them in Python and pandas.
Python'NoneType' object is not subscriptable
Fix 'NoneType' object is not subscriptable: common causes (list.sort(), dict.get, re.match, JSON null), reading the traceback, and catching it with mypy.