Skip to main content

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.

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.'}
tip

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
warning

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
note

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​

MethodAccuracySpeedDependenciesUse Case
email-validator (with DNS)⭐⭐⭐⭐⭐SlowYesUser registration
email-validator (syntax only)⭐⭐⭐⭐FastYesForm validation
Regex⭐⭐FastestNoLogging, quick scripts
email.utils.parseaddr⭐FastNoParsing 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.