How to Validate Email Addresses in Python
Email validation is deceptively complex. The official RFC 5322 specification allows formats that surprise most developers, while simple regex patterns reject valid addresses. Use purpose-built libraries for reliable validation.
Using email-validator (Recommended)β
The email-validator library checks syntax according to RFC standards and optionally verifies domain deliverability via DNS:
pip install email-validator
from email_validator import validate_email, EmailNotValidError
def check_email(email):
try:
result = validate_email(email, check_deliverability=True)
return result.normalized
except EmailNotValidError as e:
return None
# Valid email
print(check_email("user@gmail.com"))
# 'user@gmail.com'
# Invalid domain
print(check_email("user@fake-domain-xyz.com"))
# None (DNS check fails)
# Invalid syntax
print(check_email("not-an-email"))
# None
Output:
user@gmail.com
None
None
For user registration forms, return helpful error messages:
from email_validator import validate_email, EmailNotValidError
def validate_user_email(email):
"""Validate email and return normalized form or error message."""
try:
result = validate_email(
email,
check_deliverability=True,
allow_smtputf8=True # Support international emails
)
return {"valid": True, "email": result.normalized}
except EmailNotValidError as e:
return {"valid": False, "error": str(e)}
print(validate_user_email("TEST@Gmail.Com"))
# {'valid': True, 'email': 'TEST@gmail.com'}
print(validate_user_email("missing@domain"))
# {'valid': False, 'error': 'The domain name domain is not valid...'}
Output:
{'valid': True, 'email': 'TEST@gmail.com'}
{'valid': False, 'error': 'The part after the @-sign is not valid. It should have a period.'}
The normalized property returns a cleaned version of the email with proper domain casing. Store this normalized form in your database to prevent duplicate accounts with different casings.
Quick Regex Validationβ
For simple scripts where dependencies aren't justified, a basic regex catches obvious errors:
import re
def is_valid_email(email):
"""Basic email format check. Not RFC-compliant."""
pattern = r'^[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}$'
return bool(re.fullmatch(pattern, email))
print(is_valid_email("user@example.com")) # True
print(is_valid_email("invalid")) # False
print(is_valid_email("user@domain")) # False
Regex validation is inherently flawed for emails:
- Rejects valid addresses:
"john doe"@example.com,user+tag@example.com(some patterns) - Accepts invalid domains:
user@nonexistent-domain.com - Misses internationalized emails:
η¨ζ·@δΎε.δΈε½
Only use regex for logging or non-critical validation.
Using Python's Built-in Email Parserβ
The standard library can parse email addresses but doesn't validate them:
from email.utils import parseaddr
def parse_email(email):
"""Extract name and address from email string."""
name, addr = parseaddr(email)
return addr if '@' in addr else None
print(parse_email("John Doe <john@example.com>"))
print(parse_email("plain@example.com"))
print(parse_email("invalid"))
Output:
john@example.com
plain@example.com
None
This is useful for extracting addresses from formatted strings but doesn't verify validity.
Validation Without DNS Checksβ
DNS validation requires network access and adds latency. Disable it for offline validation or faster processing:
from email_validator import validate_email, EmailNotValidError
def validate_syntax_only(email):
"""Check email syntax without DNS lookup."""
try:
result = validate_email(email, check_deliverability=False)
return result.normalized
except EmailNotValidError:
return None
# Works offline, faster
print(validate_syntax_only("user@any-domain.com"))
Output:
user@any-domain.com
Batch Validationβ
For validating email lists efficiently:
from email_validator import validate_email, EmailNotValidError
def validate_email_list(emails):
"""Validate multiple emails, return valid and invalid separately."""
valid = []
invalid = []
for email in emails:
try:
result = validate_email(email, check_deliverability=False)
valid.append(result.normalized)
except EmailNotValidError as e:
invalid.append({"email": email, "error": str(e)})
return {"valid": valid, "invalid": invalid}
emails = [
"good@example.com",
"also.valid+tag@domain.org",
"bad-email",
"@missing-local.com"
]
results = validate_email_list(emails)
print(f"Valid: {len(results['valid'])}")
print(f"Invalid: {len(results['invalid'])}")
Output:
Valid: 2
Invalid: 2
Common Edge Casesβ
from email_validator import validate_email, EmailNotValidError
test_cases = [
"simple@example.com", # β
Valid
"very.common@example.com", # β
Valid
"user+tag@example.com", # β
Valid (plus addressing)
"user@subdomain.example.com", # β
Valid
"user@123.123.123.123", # β
Valid (IP address)
"plainaddress", # β Missing @
"@no-local-part.com", # β Missing local part
"user@.invalid.com", # β Invalid domain
]
for email in test_cases:
try:
validate_email(email, check_deliverability=False)
print(f"β
{email}")
except EmailNotValidError:
print(f"β {email}")
Output:
β
simple@example.com
β
very.common@example.com
β
user+tag@example.com
β
user@subdomain.example.com
β user@123.123.123.123
β plainaddress
β @no-local-part.com
β user@.invalid.com
Method Comparisonβ
| Method | Accuracy | Speed | Dependencies | Use Case |
|---|---|---|---|---|
email-validator (with DNS) | βββββ | Slow | Yes | User registration |
email-validator (syntax only) | ββββ | Fast | Yes | Form validation |
| Regex | ββ | Fastest | No | Logging, quick scripts |
email.utils.parseaddr | β | Fast | No | Parsing formatted strings |
Summaryβ
Never rely on regex alone for email validation in production systems. The email-validator library handles RFC-compliant syntax checking and optional DNS verification. Use syntax-only validation for speed when deliverability isn't critical, and enable DNS checks for user registration flows where catching typos matters.