Yes—but a Python script can check whether an address is correctly formatted and whether its domain appears able to receive mail; it cannot prove that a particular mailbox exists or will accept your campaign. The workflow below reads a CSV, preserves every row, checks syntax and optionally domain DNS, and writes a separate results file so uncertain cases can be reviewed instead of silently discarded.
What a bulk email check can—and cannot—tell you
Syntax validation catches malformed address forms, such as a missing domain or an invalid character. A DNS/MX lookup can indicate whether the domain has mail-routing records. Neither check confirms that an individual mailbox exists, that its owner consented to receive your email, or that your message will reach the inbox.
As an Amazon Associate I earn from qualifying purchases.
For that reason, the script labels a row syntax_ok, not “deliverable.” A domain-level failure is marked separately, and an unexpected lookup or processing problem becomes review. Those distinctions matter: temporary DNS trouble should not be mistaken for a permanently invalid recipient.
Prepare the CSV and install the validator
Use a CSV with a header row and identify the address column explicitly. The example assumes the column is named email; change that value in the script if your header differs. It preserves the original columns and adds a row number, status, reason, and normalized address to a new output file.
#1 Best Overall
Install the maintained email-validator package and its DNS dependency with:
python -m pip install email-validator dnspython
Keep the input file unchanged. The original address is retained in the output for auditing, while the library’s normalized form is used to detect repeated addresses. Normalization is for comparison; it is not a reason to rewrite unrelated contact data.
Rank #2
Run the CSV check
Save this as check_emails.py. It checks syntax and domain deliverability signals, reuses a caching DNS resolver for the batch, and writes results to email_check_results.csv.
import csv
from email_validator import (
EmailNotValidError,
EmailSyntaxError,
EmailUndeliverableError,
caching_resolver,
validate_email,
)
INPUT_FILE = "contacts.csv"
OUTPUT_FILE = "email_check_results.csv"
EMAIL_COLUMN = "email"
# Reuse DNS results across rows and bound lookup time.
resolver = caching_resolver(timeout=10)
seen = set()
with open(INPUT_FILE, 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 a '{EMAIL_COLUMN}' column")
fieldnames = list(reader.fieldnames) + [
"source_row", "check_status", "check_reason", "normalized_email"
]
with open(OUTPUT_FILE, "w", newline="", encoding="utf-8") as destination:
writer = csv.DictWriter(destination, fieldnames=fieldnames)
writer.writeheader()
for row_number, row in enumerate(reader, start=2):
original = row.get(EMAIL_COLUMN) or ""
address = original.strip()
row.update({
"source_row": row_number,
"check_status": "review",
"check_reason": "",
"normalized_email": "",
})
if not address:
row["check_status"] = "syntax_invalid"
row["check_reason"] = "empty address"
else:
try:
result = validate_email(
address,
check_deliverability=True,
dns_resolver=resolver,
)
normalized = result.normalized
row["normalized_email"] = normalized
if normalized in seen:
row["check_status"] = "review"
row["check_reason"] = "duplicate normalized address"
else:
seen.add(normalized)
row["check_status"] = "syntax_ok"
row["check_reason"] = "syntax valid; domain DNS check passed"
except EmailSyntaxError as error:
row["check_status"] = "syntax_invalid"
row["check_reason"] = str(error)
except EmailUndeliverableError as error:
row["check_status"] = "domain_unavailable"
row["check_reason"] = str(error)
except EmailNotValidError as error:
row["check_status"] = "review"
row["check_reason"] = str(error)
except Exception as error:
# Includes unexpected or temporary DNS/library failures.
row["check_status"] = "review"
row["check_reason"] = f"lookup or processing issue: {error}"
writer.writerow(row)
print(f"Wrote {OUTPUT_FILE}")
Interpret the output conservatively
syntax_invalid: the address is empty or failed syntax validation. Inspect the reason before correcting or removing it.domain_unavailable: the validator reported a domain-level mail problem. This is not a finding about whether a specific mailbox exists.syntax_ok: syntax passed and the domain DNS check returned a usable signal. This does not establish mailbox existence or campaign delivery.review: the address was duplicated after normalization, or the check raised an inconclusive or unexpected error. Resolve these rows deliberately rather than treating them as invalid.
The row number refers to the CSV record’s line position, beginning with the header on line 1. Keep it, or use an existing stable contact ID, to join results back to other records without relying on email addresses as unique identifiers.
Rank #3
Test a sample and handle uncertain results
Run the script on a copy containing a small sample first. Confirm that the header matches, that every input record appears in the output, and that reasons are understandable. Inspect review and domain_unavailable rows before deciding what to do; DNS can be slow or unreliable, and a transient failure is not proof that an address is bad.
For large batches, the reusable resolver caches repeated DNS lookups and the ten-second timeout bounds each lookup. A timeout or other transient failure should remain a review item, not be converted into a definitive invalid result. If your environment produces DNS exceptions not handled by the library’s validation exceptions, this example records them as review; check the error and retry selectively if appropriate.
Rank #4
Why the script does not probe mailboxes with SMTP
Do not make SMTP mailbox probing the default next step. Python’s smtplib exposes SMTP VRFY, but the Python documentation notes, “Many sites disable SMTP VRFY in order to foil spammers.” Python’s smtplib documentation describes the command; it is not a universal bulk-recipient verification service.
Even when a server appears to accept a recipient, that response may be ambiguous or temporary. Privacy protections, greylisting, temporary failures, and delayed bounces can all undermine a probe’s result. The email-validator project documentation explains these limits and why contacting SMTP servers offers little benefit for this purpose.
Best Value
List cleanup is separate from sending compliance
Verification does not replace permission to contact recipients, a working unsubscribe process, or sender authentication. Google says all senders must set up SPF or DKIM, while bulk senders must set up SPF, DKIM, and DMARC; authentication can reduce rejection or spam classification risk but does not guarantee inbox placement. See Google’s email sender guidelines.
Google’s bulk-sender classification is specific to mail sent to personal Gmail accounts: it applies to senders approaching 5,000 or more messages to those accounts within 24 hours. Google aggregates messages from subdomains under the same primary domain, and says bulk-sender status does not expire once assigned. This is Google’s threshold, not a universal definition of bulk email. Google’s sender-guidelines FAQ says enforcement of non-compliant traffic has been ramping up since November 2025, including temporary and permanent rejections; operational rules can change, so consult the current guidance when planning a Gmail campaign.
Quick Recap
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.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.




