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-validatorfrom 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
print(check_email("user@gmail.com"))
print(check_email("user@fake-domain-xyz.com"))
print(check_email("not-an-email"))Output:
user@gmail.com
None
NoneFor 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"))
print(validate_user_email("missing@domain"))Output:
{'valid': True, 'email': 'TEST@gmail.com'}
{'valid': False, 'error': 'The part after the @-sign is not valid. It should have a period.'}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")) # FalseUsing 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
NoneValidation 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
print(validate_syntax_only("user@any-domain.com"))Output:
user@any-domain.comBatch 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: 2Common 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.comMethod 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.