Developer Hub / Validate an email address in Python
Validate an Email Address in Python
Three ways to validate an email address in Python, from a format check to a full mailbox check. You can validate email address syntax with one library call, check the domain with a DNS lookup, and confirm the mailbox with an API. We tested each one on the same addresses, including a typo domain that fools the most popular library.
Tested with email-validator 2.3.0 and dnspython 2.9.0 on 21 October 2026.
Step 1: validate the format with email-validator
The email-validator package is the most common choice. It checks the format against the email standards, normalizes the address, and can check the domain. Install it with pip:
pip install email-validator
from email_validator import validate_email, EmailNotValidError
def check(address: str, deliverability: bool = True):
try:
info = validate_email(address, check_deliverability=deliverability)
return True, info.normalized
except EmailNotValidError as err:
return False, str(err)
Results from our test run:
| Address | Result | Message or normalized address |
|---|---|---|
Jane.Smith@Example.co.uk | valid | Jane.Smith@example.co.uk (domain lowercased) |
name@gmailcom | invalid | The part after the @-sign is not valid. It should have a period. |
jane..smith@example.com | invalid | An email address cannot have two periods in a row. |
jane@example.com | invalid | The domain name example.com does not accept email. |
jane@this-domain-does-not-exist-91827.com | invalid | The domain name this-domain-does-not-exist-91827.com does not exist. |
jürgen@example.de | valid | International addresses are supported |
jane@gmial.com | valid | Passes, even though gmial.com is a typo. See below. |
Use info.normalized, not the raw input, when you save the address. It lowercases the domain and applies the standard Unicode form, so the same person is not stored twice. Older code and tutorials often read .email instead. That still works, but .normalized is the name current versions document.
The deliverability check makes a DNS query, so it adds a short delay. For bulk imports or offline tests, pass check_deliverability=False to check only the format.
Why jane@gmial.com passes
gmial.com has no MX records, so it has no mail servers. But it does have an A record, the address of a website. The email standard (RFC 5321) says a sending server may fall back to the A record when there are no MX records, and email-validator follows that rule. So the typo passes.
That is correct by the standard, but it is not what you want on a signup form. Almost every real mailbox domain publishes MX records, and a typo of a big provider almost never belongs to a real customer.
Step 2: require MX records with dnspython
To catch typo domains, check for MX records yourself. dnspython is installed with email-validator, or on its own with pip install dnspython.
import dns.resolver
def domain_accepts_mail(domain: str) -> bool:
try:
answers = dns.resolver.resolve(domain, "MX")
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
return False
hosts = [str(r.exchange) for r in answers]
# A null MX (RFC 7505) points to "." and means the domain takes no mail.
return any(h not in (".", "") for h in hosts)
| Domain | Result | Why |
|---|---|---|
gmail.com | True | Has MX records |
gmial.com | False | No MX records, only an A record |
example.com | False | Publishes a null MX, so it accepts no mail |
Timeouts raise a different error, dns.resolver.LifetimeTimeout. Treat that as "unknown" and try again later, rather than rejecting the address.
Step 3: confirm the mailbox with an email verification API
The first two steps prove the address is well formed and the domain can receive mail. They cannot tell you whether jane has a mailbox at that domain. That needs an SMTP check against the mail server. Most cloud hosts block outbound port 25, so in practice this check runs through an email verification API.
import os
import requests
def verify_mailbox(address: str) -> str:
response = requests.post(
"https://api.giggal.ai/v1/verify",
json={"email": address},
headers={"Authorization": f"Bearer {os.environ['GIGGAL_API_KEY']}"},
timeout=30,
)
result = response.json()
if not result.get("success"):
raise RuntimeError(result.get("error"))
return result["data"]["status"] # deliverable, undeliverable or unknown
The API also handles catch-all domains, where the mail server accepts every address. It returns a valid or invalid result for them in the catch_all_verdict field. New accounts get 1,000 free credits. The Developer Hub lists every endpoint, including the bulk email verification endpoint for lists of up to 50,000 addresses.
Putting it together
Run the cheap checks first and call the API only for addresses that pass them. This saves credits and keeps your form fast.
def validate(address: str) -> str:
ok, value = check(address, deliverability=False) # format only, no network
if not ok:
return "invalid: " + value
domain = value.split("@")[1]
if not domain_accepts_mail(domain):
return "invalid: domain has no mail servers"
return verify_mailbox(value) # mailbox check via the API
What about a regex?
A regex is fine for a quick hint in a script, but it is a poor final check. Our JavaScript email validation tests show how common patterns accept addresses like .jane@example.com and jane..smith@example.com, which mail servers reject. The same patterns behave the same way in Python's re module. email-validator already handles these cases, so prefer it over writing your own pattern.
If you only need to check one address by hand, paste it into the free email verifier instead.
Frequently asked questions
What is the best way to validate an email address in Python?
Use the email-validator library for the format and domain check, and an email verification API when you need to know that the mailbox exists. Avoid writing your own regex for production code.
Does email-validator check if the email exists?
No. With check_deliverability on, it checks that the domain can receive mail. It does not contact the mail server, so it cannot tell whether the mailbox exists.
Can Python check a mailbox over SMTP directly?
Yes, with smtplib, but outbound port 25 is blocked on most cloud servers, many mail servers rate limit or greylist unknown senders, and catch-all domains accept every address. These are the reasons most teams use an API for this step.