email-validator, a Python email checker that normalizes before you store it
A robust email syntax and deliverability validation library for Python.
At a glance
- What is it?
- email-validator is a Python library that checks whether a string has the form [email protected], optionally verifies that the domain has DNS records for receiving mail, and returns a normalized form you are meant to put in your database. It is opinionated, rejects obsolete and special-use syntax by default, and supports internationalized domains and local parts.
- Who is it for?
- Adopt email-validator if you are identifying users by email address and want both a syntax check and a normalized form you can key a database on, and skip it if you need a validator that accepts the full range of technically valid historical syntax, because this one is deliberately opinionated and points you at pyIsEmail as the permissive alternative.
- Can I use it commercially?
- Yes. Unlicense is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 8 days ago.
- What is it written in?
- Mainly Python, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The one function that does the work
The library exposes a single function, validate_email, which takes an address and either raises an exception explaining what is wrong or returns an object containing the normalized form of the address and other information about it. The exception hierarchy is worth knowing because it is how you distinguish a typo from a dead domain. When an address is not valid, validate_email raises an EmailSyntaxError if the form of the address is invalid, or an EmailUndeliverableError if the domain name fails the DNS checks. Both are subclasses of EmailNotValidError, which in turn subclasses ValueError, so a caller that only wants to know whether the address is usable can catch the common base class and a caller that wants to distinguish the two cases can catch the specific ones. The error message is described as a human-readable explanation of why it is not a valid or deliverable address, and the library makes a point of that being a feature you can show to an end user rather than a stack trace.
from email_validator import validate_email, EmailNotValidError
email = "[email protected]"
try:
emailinfo = validate_email(email, check_deliverability=False)
email = emailinfo.normalized
except EmailNotValidError as e:
print(str(e))Normalization is the part that quietly prevents duplicate accounts
The recommendation to store the normalized form is not a stylistic suggestion, it is the mechanism that keeps one person from having two accounts. The documentation says to put the normalized form in your database and to always normalize before checking whether an address is already in that database. That matters most for the cases that look different but are the same mailbox: internationalized addresses and quoted-string addresses, where the display form and the transport form differ. If you store what the user typed and compare with a raw SQL equality, `ツ@example.com` and its punycode or quoted equivalent are two rows. If you store the normalized form and compare normalized, they are one. The library also says the README is occasionally ahead of the latest published package and points to the CHANGELOG for the version history, so if you are chasing a behaviour difference, the changelog rather than the README is the authority.
pip install email-validatorWhat deliverability checking actually does, and does not, do
The optional deliverability check asks the DNS whether the domain can receive mail, which is more than a syntax check but much less than proof of delivery. The detail is specific: the validator checks for a DNS MX record, and except for a Null MX record, if there is no MX record a fallback A or AAAA record is permitted unless a reject-all SPF record is present. That last clause is the interesting one, because it is a rare case where the library consults SPF to avoid false positives. The library is explicit that it does not try to contact an SMTP server, and gives three reasons: there is nothing to be gained, servers are good at not revealing whether an address is deliverable, and addresses that appear to accept mail can bounce later, with a bounced message sometimes indicating a temporary failure such as greylisting. The practical consequence is that a pass from this library means the domain is plausibly configured, not that the mailbox exists. The documentation also warns that DNS is slow and sometimes unavailable, and tells you to consider whether the checks are useful for your case at all.
check_deliverability, and the login versus signup split
The check_deliverability keyword argument defaults to True, and turning it off is a decision with a specific recommendation attached. The documentation says to pass False when validating for login pages but not for account creation pages, since re-validating a previously validated domain by querying DNS at every login is probably undesirable. The logic is sound: a domain that resolved when the account was created may not resolve now, and blocking a returning user on that basis is a bad trade, whereas at creation time a DNS check is exactly the moment you want to know. For applications that always want it off, there is a module-level switch, email_validator.CHECK_DELIVERABILITY, set to False to turn it off for all calls by default. The same pattern appears for test_environment, which can be set on the module as email_validator.TEST_ENVIRONMENT. The DNS resolver itself is injectable: pass a dns.resolver.Resolver instance to control the resolver including a timeout and a cache, and a helper function is provided to construct one with an LRUCache, which you should reuse across calls so the cache does something.
emailinfo = validate_email(email, check_deliverability=False)The opinions, and why the author says so twice
This library tells you it is opinionated, and it says so in the feature list and then again in a separate paragraph, which is unusual and worth taking seriously. By default it rejects addresses with invalid or unsafe Unicode characters, obsolete email address syntax that you would find unexpected, special use domain names such as @localhost, and domains without a dot. The reasoning behind the obsolete-syntax rejection is given: those forms are still valid and deliverable, but nobody uses them, and they will probably cause grief if you are using email for login. The author then recommends the less opinionated pyIsEmail if this one does not fit, which is a rare and welcome bit of honesty from a library author. The other opinionated choices are on the permissive side. Internationalized domain names such as @ツ.life and internationalized local parts such as ツ@example.com are supported, display names such as "My Name" <[email protected]> can optionally be parsed, and there is an allow_smtputf8 option that can be set to False to prohibit addresses that would need the SMTPUTF8 extension. The README also describes how to allow some obsolete forms if you need them.
The test environment switch, and the typing discipline
The test_environment option is a small thing with a big effect on a test suite. When it is True, the DNS based deliverability checks are disabled and test and **.test domain names are permitted, which means your unit tests can use addresses under the.test domain without either stubbing the resolver or hitting the network. The project makes the same distinction inside its own test suite, where pytest is configured with a network marker for tests that require Internet access, and the Makefile runs the test target with a filter that excludes the network tests, so the default run is offline and deterministic. The typing setup is unusually strict for a library of this size, with mypy configured to disallow any generics, subclassing of Any, untyped defs, incomplete defs, untyped calls and untyped decorators, plus checking of untyped defs, warning on redundant casts and on unused ignores. That is the level of configuration you would expect in a library that is meant to be trusted at a signup form, and it costs the author nothing at runtime.
Project shape, packaging and licence
The repository is a conventional Python library tree: an email_validator/ package directory, a tests/ directory, a pyproject.toml, a setup.cfg, a setup.py that is now just a call to setup, a Makefile, a MANIFEST.in, a test_requirements.txt, a release_to_pypi.sh script, a CHANGELOG.md, a CONTRIBUTING.md and the LICENSE file. The pyproject.toml holds the mypy and pytest configuration, and the Makefile wires the developer workflow into four targets: install, which upgrades setuptools and pip, installs the test requirements and does an editable install of the package; lint, which runs flake8 over the package and the tests with a few specific ignores; typing, which runs mypy over the same files; and test, which runs pytest with coverage over the package and excludes the network tests. The default make goal runs typing, test coverage and lint in sequence, so a contributor who runs make gets the full check. The licence is the Unlicense, which is a public domain dedication and also a permissive licence, and the project is on PyPI under the name email-validator. The repository is not archived, with the last push on 2026-10-15 and the most recent release v2.3.0 from 2025-08-26.
Editorial conclusion
Adopt email-validator if you are identifying users by email address and want both a syntax check and a normalized form you can key a database on, and skip it if you need a validator that accepts the full range of technically valid historical syntax, because this one is deliberately opinionated and points you at pyIsEmail as the permissive alternative. Verify three things before you wire it into a signup flow: that you call it with check_deliverability enabled on account creation and disabled on login, since the documentation recommends exactly that split; that you store the normalized form rather than what the user typed; and that you supply a caching DNS resolver if you keep deliverability checks on, because the DNS lookups are the slow and unreliable part.
Frequently asked questions
What does the email-validator Python library do?
It validates that a string is of the form [email protected] and optionally checks that the domain is set up to receive email by looking at DNS. It also normalizes addresses and returns friendly English error messages, and it supports internationalized domains, internationalized local parts and display names.
How do I install email-validator and validate an address?
Install it with `pip install email-validator`, then call `validate_email(email, check_deliverability=...)` inside a try block that catches `EmailNotValidError`. The exception message is a human-readable explanation you can show to the user, and on success you get an object with the `normalized` form of the address.
Should I use check_deliverability on a login page?
No. The documentation recommends passing False for login pages and True for account creation pages, because re-querying DNS for a domain already in your database on every login is probably undesirable. It can also be disabled globally with `email_validator.CHECK_DELIVERABILITY`.
Does email-validator check that the mailbox actually exists?
No, and it explains why. It never contacts an SMTP server: there is nothing to be gained, servers are good at not revealing whether an address is deliverable, and an address that appears to accept mail can bounce later, sometimes deliberately through greylisting. The check is a DNS MX lookup with an A or AAAA fallback.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/joshdata-python-email-validator)Was this useful?