email-validator review
email-validator 2.3.0 turns an email-address string into a normalized record or a specific syntax or DNS exception. It handles Unicode local parts, IDNA domains, quoted local parts, display names, and domain literals through explicit options. The optional deliverability check looks for MX records, Null MX, and limited A or AAAA fallbacks, but it does not contact an SMTP server or prove that a mailbox exists. Version 2.3.0 made the 64-character local-part check opt-in with `strict=True`, added NFC normalization for display names, and now raises `TypeError` for values other than strings or bytes. The package is typed, pure Python, and requires Python 3.8 or newer.
email-validator 2.3.0 installed in 0.2 seconds and occupied 2 MB across 3 packages in our sandbox, with typed Python and 0 audit findings. Install it at an account boundary when normalized international addresses matter, but skip its DNS pass on login and use a confirmation email to establish ownership.
We installed it
| Install | ✓ · 0.2s | 3 packages on disk · 2 MB |
| Import | ✓ | import email_validator in 0.11s · pure Python · py.typed · requires Python >=3.8 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does email-validator install cleanly?
Yes. In a fresh container with an empty cache, pip install email-validator finished in 0.2s, leaving 3 packages and 2 MB on disk. pip-audit reported no known vulnerabilities.
What does email-validator need to run?
Python >=3.8, and nothing compiled: it is pure Python. In our run import email_validator succeeded in 0.11s, and the package ships py.typed for type checkers.
email-validator or pyIsEmail: which should you use?
pyIsEmail: Choose it when RFC diagnosis and acceptance of uncommon address forms matter more than signup policy. email-validator 2.3.0 installed in 0.2 seconds and occupied 2 MB across 3 packages in our sandbox, with typed Python and 0 audit findings.
When should you not use email-validator?
You need a parser that accepts obsolete or unusual RFC forms. The project deliberately rejects comments and other addresses that are legal on paper but troublesome in account systems; pyIsEmail is the documented less-opinionated option.
Discussed on
Use it if
- A registration endpoint must store one normalized form so case, Unicode, and IDNA variations do not create accidental duplicate accounts.
- Your mail path accepts international addresses and needs the returned `smtputf8`, `ascii_domain`, and `ascii_email` fields to choose a delivery route.
- Signup validation should distinguish malformed syntax from a domain that cannot receive mail, while still leaving mailbox ownership to a confirmation message.
- A Pydantic model uses `EmailStr`; Pydantic delegates that address validation to this package when its email extra is installed.
- You need a parser that accepts obsolete or unusual RFC forms. The project deliberately rejects comments and other addresses that are legal on paper but troublesome in account systems; pyIsEmail is the documented less-opinionated option.
- Every request must remain free of DNS latency. Deliverability checking defaults to on and the documented resolver timeout is 15 seconds unless you disable the check or pass a resolver with a shorter timeout.
- The decision requires proof that the mailbox exists. MX or A records only describe a domain, and the README explicitly declines SMTP probing because it cannot establish mailbox ownership.
- Your application must keep the submitted spelling as its lookup key. The result can normalize Unicode, IDNA domains, quotes, and escapes, so the documented database value is `normalized`, not the original input.
- You only want a yes-or-no format predicate shared with URL, IP, and hostname checks. The `validators` package fits that narrower utility role without exposing DNS and mail-system fields.
Setup reality
We installed email-validator 2.3.0 in a fresh Python 3.12 Bookworm sandbox. The install finished in 0.2 seconds, left 3 packages using 2 MB, and declared 2 direct dependencies. pip-audit reported 0 known vulnerabilities. The package is pure Python, includes py.typed, requires Python 3.8 or newer, and import email_validator completed in 0.11 seconds. No compiler or system headers were needed.
DNS is the first setting to settle. check_deliverability=True is the default, so a validation call can query MX, A, AAAA, and a limited SPF condition. Turn it off when normalizing a login for an account whose address was already accepted. For imports or signup bursts, create one caching_resolver(timeout=...) and reuse it; the cache lives on that resolver instance. A temporary timeout may return a result with missing MX detail rather than proving the domain is permanently bad.
Persist result.normalized, then run the same normalization before a database lookup. Unicode domains also expose ascii_domain; ascii_email becomes None when the local part itself needs SMTPUTF8. Version 2.3.0 leaves local parts longer than 64 characters alone by default, while strict=True restores that compatibility check. Display names, quoted local parts, address literals, and empty local parts remain opt-in because many login and mail systems do not accept them consistently.
Fixture domains need care. test_environment=True permits test and its subdomains and also disables the DNS pass. Addresses under example.com can pass syntax parsing but are poor positive deliverability fixtures because their DNS is not set up to receive mail. Catch EmailSyntaxError and EmailUndeliverableError separately if the UI treats correction differently from a domain failure. Both derive from EmailNotValidError; neither replaces sending a confirmation link.
Patterns
Normalize a signup address normalize-signup
from email_validator import EmailNotValidError, validate_email
try:
info = validate_email(submitted_email)
except EmailNotValidError as exc:
form_errors["email"] = str(exc)
else:
account.email = info.normalized`normalized` is the documented database value in 2.3.0. Run validation again before using later input as a lookup key.
Normalize login without DNS normalize-login
from email_validator import validate_email
email_key = validate_email(
submitted_email,
check_deliverability=False,
).normalized
account = find_account(email=email_key)`check_deliverability=False` avoids a DNS request on every login for an address already accepted during registration.
Reuse one DNS resolver cache-dns-lookups
from email_validator import caching_resolver, validate_email
resolver = caching_resolver(timeout=5)
validated = [
validate_email(value, dns_resolver=resolver)
for value in submitted_addresses
]The DNS cache belongs to the resolver instance. Rebuilding it inside the loop loses reuse, and 5 seconds replaces the documented 15-second default timeout.
Separate syntax from domain failure classify-errors
from email_validator import EmailSyntaxError, EmailUndeliverableError, validate_email
try:
info = validate_email(address)
except EmailSyntaxError as exc:
return {"kind": "syntax", "message": str(exc)}
except EmailUndeliverableError as exc:
return {"kind": "domain", "message": str(exc)}Both exceptions inherit `EmailNotValidError` in 2.3.0. A domain error still says nothing about whether the named mailbox exists.
Inspect international address output inspect-smtputf8
from email_validator import validate_email
info = validate_email(
"ユーザー@ツ.life",
check_deliverability=False,
)
print(info.normalized)
print(info.ascii_domain)
print(info.ascii_email)
print(info.smtputf8)`ascii_email` is `None` when the local part has non-ASCII characters. A true `smtputf8` value means the outbound mail path must support SMTPUTF8.
Reject addresses the relay cannot send forbid-smtputf8
from email_validator import validate_email
info = validate_email(
address,
allow_smtputf8=False,
check_deliverability=False,
)`allow_smtputf8=False` rejects a non-ASCII local part instead of returning an address that an ASCII-only relay cannot transmit.
Parse a display name deliberately parse-display-name
from email_validator import validate_email
info = validate_email(
"Billing Desk <billing@example.org>",
allow_display_name=True,
check_deliverability=False,
)
print(info.display_name, info.normalized)Display names are rejected by default. Version 2.3.0 returns the enabled `display_name` after Unicode NFC normalization.
Enable the strict local-part limit enforce-local-length
from email_validator import validate_email
info = validate_email(
address,
strict=True,
check_deliverability=False,
)Version 2.3.0 no longer applies the 64-character local-part ceiling by default. `strict=True` turns that one extra syntax check back on.
Accept a reserved test domain use-test-address
from email_validator import validate_email
info = validate_email(
"person@accounts.test",
test_environment=True,
)
assert info.normalized == "person@accounts.test"`test_environment=True` permits `test` subdomains and disables DNS deliverability. Keep that switch out of production configuration.
Opt into a quoted local part allow-quoted-local
from email_validator import validate_email
info = validate_email(
'"sales team"@example.org',
allow_quoted_local=True,
check_deliverability=False,
)
print(info.normalized)Quoted local parts are off by default because account and mail systems often mishandle them. Normalization removes quotes or escapes when they are unnecessary.
Read an IP address literal allow-address-literal
from email_validator import validate_email
info = validate_email(
"operator@[192.0.2.10]",
allow_domain_literal=True,
)
print(info.domain_address)Domain literals require an explicit option and skip normal deliverability checks. The parsed IP object is available as `domain_address`.
Put an email in a Pydantic model validate-pydantic-field
from pydantic import BaseModel, EmailStr
class Registration(BaseModel):
email: EmailStr
registration = Registration(email="person@example.org")Pydantic needs its email extra so this package is installed. `EmailStr` validates syntax without performing the DNS deliverability pass.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| pyIsEmail | PyPI | Choose it when RFC diagnosis and acceptance of uncommon address forms matter more than signup policy. |
| validators | PyPI | Choose it for simple predicates covering email, URLs, IP addresses, and other common strings. |
| pydantic | PyPI | Choose `EmailStr` when email parsing belongs inside a larger typed request model and direct DNS control is unnecessary. |
More web backend guides
urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

