Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Yes—Python can flag malformed email addresses and domains that appear unable to receive mail, but no format or DNS check can prove that a particular mailbox exists or will accept your campaign. A cautious pre-send workflow keeps the original CSV intact, records a status for every row, and leaves uncertain cases for review instead of labeling them deliverable.

What a bulk email check can—and cannot—tell you

Syntax validation checks whether an address has a recognizable email format. A DNS/MX lookup checks whether its domain advertises a route for receiving email. These checks can identify some obvious problems, but neither proves that an individual mailbox exists, is active, or will accept a message. A recipient server may reject a message for reasons unrelated to address format or domain records.

For that reason, label a passing row syntax_ok, not “valid,” “verified,” or “deliverable.” Keep temporary lookup failures and other inconclusive outcomes separate from addresses with clear syntax or domain problems.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Prepare the CSV and Python environment

Install the maintained email-validator package in your environment:

python -m pip install email-validator

Before running a batch, identify the email column by its actual header and choose an input and output filename. The example below expects an email column; change that constant if your CSV uses another name. It assigns a stable row number, retains every original field and the exact input address, and writes results to a separate file.

Run a cautious batch check

This script first checks syntax without DNS. Only syntactically acceptable addresses proceed to the optional DNS-based check. A reusable caching resolver avoids repeating some lookups during the batch and uses a bounded timeout. DNS can still be slow or unreliable, so the script records uncertain lookup outcomes as review.

import csv
from email_validator import (
    EmailNotValidError,
    EmailUndeliverableError,
    caching_resolver,
    validate_email,
)

INPUT_CSV = "contacts.csv"
OUTPUT_CSV = "contacts_check_results.csv"
EMAIL_COLUMN = "email"

# Reuse one resolver across the batch; the timeout is in seconds.
resolver = caching_resolver(timeout=10)

with open(INPUT_CSV, "r", newline="", encoding="utf-8-sig") as source:
    reader = csv.DictReader(source)
    if not reader.fieldnames or EMAIL_COLUMN not in reader.fieldnames:
        raise ValueError(f"CSV must contain an {EMAIL_COLUMN!r} column")

    original_fields = list(reader.fieldnames)
    output_fields = ["row_id", *original_fields,
                     "email_normalized", "status", "reason"]

    with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as destination:
        writer = csv.DictWriter(destination, fieldnames=output_fields)
        writer.writeheader()

        for row_id, row in enumerate(reader, start=1):
            original = row.get(EMAIL_COLUMN) or ""
            result = {
                "row_id": row_id,
                **row,
                "email_normalized": "",
                "status": "review",
                "reason": "",
            }

            if not original.strip():
                result["status"] = "syntax_invalid"
                result["reason"] = "blank email field"
                writer.writerow(result)
                continue

            try:
                parsed = validate_email(
                    original.strip(), check_deliverability=False
                )
                result["email_normalized"] = parsed.normalized
            except EmailNotValidError as exc:
                result["status"] = "syntax_invalid"
                result["reason"] = str(exc)
                writer.writerow(result)
                continue

            try:
                validate_email(
                    parsed.normalized,
                    check_deliverability=True,
                    dns_resolver=resolver,
                )
                result["status"] = "syntax_ok"
                result["reason"] = "syntax passed; DNS check did not establish mailbox existence"
            except EmailUndeliverableError as exc:
                result["status"] = "domain_unavailable"
                result["reason"] = str(exc)
            except EmailNotValidError as exc:
                # Preserve ambiguous or unexpected validation outcomes for review.
                result["status"] = "review"
                result["reason"] = str(exc)
            except Exception as exc:
                # DNS and network failures should not silently become invalid addresses.
                result["status"] = "review"
                result["reason"] = f"DNS or lookup error: {exc}"

            writer.writerow(result)

After the run, contacts_check_results.csv contains the input columns plus row_id, email_normalized, status, and reason. The normalized value is useful for comparisons, while the original email field remains unchanged for auditing and joining results back to your source data. Do not replace source values with normalized output or discard rows just because a check raised an error.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Interpret and act on each status

  • syntax_invalid: The address is blank or the library rejected its syntax. Inspect the original value and correct it only when you can confirm the intended address.
  • domain_unavailable: The optional domain check identified a domain-level problem. Treat this as a reason to investigate or suppress the record—not proof that a particular mailbox is nonexistent.
  • review: The lookup or validation result was inconclusive or encountered an error. Keep it out of any automatic “safe to send” group until reviewed.
  • syntax_ok: The syntax check and configured DNS check completed without identifying the failures handled above. It still does not establish mailbox existence or delivery.

Start with a small sample and inspect the output and reasons before processing the full list. A DNS outage, timeout, or transient network issue can affect many rows at once; rerun or investigate those outcomes rather than converting them into permanent invalid records. Do not send a test campaign to addresses unless you have an appropriate basis to contact them.

Why this script does not probe mailboxes over SMTP

SMTP’s VRFY command is not a dependable bulk verification method. The Python Software Foundation notes, “Many sites disable SMTP VRFY in order to foil spammers.” Even when a server responds, privacy protections, greylisting, temporary failures, and delayed bounces can make the result ambiguous. The Python smtplib documentation describes the standard library’s SMTP functionality; it is not a universal mailbox-existence service. The email-validator project documentation also explains its syntax and optional DNS checks, caching resolver, and the limitations of SMTP probing.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Keep list cleanup separate from sender compliance

A cleaner list does not replace consent, unsubscribe practices, or sender authentication. Google’s published guidance says all senders need SPF or DKIM, while bulk senders must set up SPF, DKIM, and DMARC; authentication does not guarantee inbox placement. See Google’s Email sender guidelines for the requirements.

Google defines a bulk sender for its personal Gmail rules as one sending close to 5,000 or more messages to personal Gmail accounts within a 24-hour period. It aggregates mail from subdomains under the same primary domain for this threshold, and says bulk-sender classification does not expire once assigned. This is Google’s classification and recipient scope, not a universal definition of bulk email. Google’s Email sender guidelines FAQ says enforcement of non-compliant traffic has been ramping up since November 2025, with possible temporary and permanent rejections; consult the current guidance before sending because operational requirements can change.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.