mrkeyoor.com_
Sun 20 Sept 07:01 UTC
PyPIWeb Backendupdated 20 Sept 2026

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.

Verdict

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

Lab card: what happened when we installed email-validatorScreenshot of email-validator documentation
Install✓ · 0.2s3 packages on disk · 2 MB
Importimport email_validator in 0.11s · pure Python · py.typed · requires Python >=3.8
Known vulns0(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.

API stability5/5email-validator 2.3.0 still uses the long-standing `validate_email` call, a `ValidatedEmail` result, and exceptions derived from `EmailNotValidError`. The current release adds `strict=True`, changes handling of non-string inputs, and normalizes display names without replacing the ordinary validation path. Deprecated dictionary access and the old `email` result alias remain documented migration points, while `normalized` is still the field callers are told to store.
Docs5/5The README gives concrete behavior for more than 10 validation switches, every returned address and DNS field, the exception hierarchy, Unicode normalization, test domains, and resolver caching. It states that the default DNS timeout is 15 seconds and explains why login calls should disable deliverability checks. It also names forms the parser rejects on purpose and separates domain checks from mailbox verification, which prevents two common implementation mistakes.
Maintenance4/5GitHub reports 1,439 stars, 15 open issues and pull requests, an unarchived repository, and a latest push on June 26, 2026. Release 2.3.0 was published on August 26, 2025 with five focused compatibility and input-handling changes. The project is receiving work, although its small maintainer footprint and year-scale release cadence make it sensible to pin a tested 2.x version rather than assume frequent patches.
Ecosystem5/5The recorded registry figure is 49,984,221 weekly downloads, and Pydantic uses this package behind `EmailStr` when the email extra is present. Its two direct dependencies cover DNS and IDNA behavior, while the result exposes ASCII and SMTPUTF8 details that mail services can act on without adopting a web framework. That reach makes examples and integrations easy to find, though downstream users may not realize they installed it indirectly.

Discussed on

  1. hnShow HN: Deep Email Validator51 points
  2. hnShow HN: Free Email Validator – Verify emails in the browser (no signup)3 points

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.
Skip it if

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

PackageRegistryPick it when
pyIsEmailPyPIChoose it when RFC diagnosis and acceptance of uncommon address forms matter more than signup policy.
validatorsPyPIChoose it for simple predicates covering email, URLs, IP addresses, and other common strings.
pydanticPyPIChoose `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.