< Back to blog

Python Email Validation: Syntax, DNS and Mailbox Limits

Validate email syntax and DNS in Python, handle inconclusive checks, and understand why a successful check does not prove mailbox ownership.
Python Email Validation: Syntax, DNS and Mailbox Limits

Python can validate email syntax and examine a domain's mail-routing records. Those checks do not prove that a particular mailbox exists, belongs to the person entering it, or will accept the next message. This guide builds two small functions with explicit outcomes so an application can preserve that uncertainty.

Choose the evidence your application needs

A signup form needs fast, actionable feedback. An import job can allow more time for domain checks. A login form should not lock an existing user out merely because a fresh DNS lookup fails. Keep those workflows separate instead of running the same network test on every request.

Use a maintained parser for syntax, a bounded resolver for domain evidence, and a separate confirmation flow when control of the address matters. Store the time and reason for each result. Do not collapse all three into a single verified=true flag.

1. Validate syntax and normalize the address

Install the dependency with python -m pip install email-validator. The following function deliberately performs no network lookup. It accepts text, removes surrounding whitespace and returns the library's normalized address.

from email_validator import validate_email, EmailNotValidError

def check_syntax(value):
    if not isinstance(value, str):
        return {"syntax_ok": False, "reason": "Expected text"}
    try:
        result = validate_email(value.strip(), check_deliverability=False)
    except EmailNotValidError as error:
        return {"syntax_ok": False, "reason": str(error)}
    return {"syntax_ok": True, "normalized": result.normalized,
            "mailbox_checked": False}

assert check_syntax("alex+news@example.com")["syntax_ok"]
assert not check_syntax("alex@@example.com")["syntax_ok"]
assert not check_syntax(None)["syntax_ok"]

The assertions check formatting behavior only. The example.com addresses are test inputs, not claims about real recipients. Preserve the local part returned by the library; do not invent provider-specific rules that merge distinct customer identities.

2. Add a bounded domain check where useful

The library supports DNS checks and a reusable caching resolver. Its routing logic can consider A/AAAA fallback when MX is absent; a missing MX record alone is not a universal rejection rule. See the maintainer's documentation for supported behavior and options.

from email_validator import validate_email, caching_resolver
from email_validator import EmailSyntaxError, EmailUndeliverableError

resolver = caching_resolver(timeout=3)

def check_domain(value):
    if not isinstance(value, str):
        return {"state": "syntax_error"}
    try:
        result = validate_email(value.strip(), check_deliverability=True,
                                dns_resolver=resolver)
    except EmailSyntaxError:
        return {"state": "syntax_error"}
    except EmailUndeliverableError as error:
        return {"state": "domain_check_failed", "reason": str(error)}
    # A temporary DNS problem may return no MX evidence without raising.
    return {"state": "domain_route_found" if getattr(result, "mx", None)
            else "domain_check_inconclusive",
            "normalized": result.normalized, "mailbox_checked": False}

The three-second resolver setting is an example DNS budget, not a guaranteed end-to-end response time. A background job also needs its own deadline, concurrency limit and bounded retries. Keep an inconclusive result pending rather than treating a timeout as a nonexistent mailbox.

3. Keep domain evidence separate from mailbox evidence

domain_route_found is an application label in this example. It is not an Email Awesome verification result. The function makes no SMTP connection and sets mailbox_checked to false even after finding a route.

An SMTP acceptance response can still be inconclusive on an accept-all domain or precede a later delivery failure. A rejection can reflect policy rather than a nonexistent recipient. Do not convert an isolated SMTP response into proof of ownership, consent or inbox placement.

Use the result safely in a production workflow

  • Show a correction message for malformed input before writing it to a sendable list.
  • Keep temporary network failures in a pending queue with bounded retry rules.
  • Store verification time and result separately from confirmation and unsubscribe state.
  • Keep API credentials server-side and implement the selected provider's documented response contract.
  • Confirm address control with a requested confirmation message when the workflow requires it.

For the policy behind these distinctions, read what syntax checks cannot establish. For the same separation in a JavaScript backend, use the Node.js validation example.

Explore the related Email Awesome workflow and review its current setup before implementing it.

Add email validation wherever data enters your system

Connect Email Awesome to products, signup forms, CRMs, lead workflows, and background jobs with programmable verification.

Free Download
Clean email lists before your next campaign
Clean email lists before your next campaign

Upload a CSV or TXT file and separate valid, invalid, unknown, disposable, and catch-all results before your next campaign.

Free Download

Get

80%

Off

First month on the 2,000-validations plan with code:

FIRSTPURCHASE
Redeem My Code

Frequently Asked Questions

Check the most Frequently Asked Questions

What is the best Python library for email validation?

How do I validate email syntax in Python using Regex?

How does Python check if an email domain exists?

Can Python verify an email without sending a message?

Why is my Python SMTP script failing on Yahoo or Gmail?

Latest
Posts

Actionable tips, current trends, and step-by-step guides to help your campaigns move from "delivered" to "adored."

View all posts