Skip to content

How to Validate Email Addresses in Python

Learn how to properly validate email addresses in Python using RFC-compliant methods like the email-validator library, including syntax checks, DNS verification, and handling edge cases.

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

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
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"))

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"))        # False

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

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

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.

Navigation