High bounce rates get your sending domain throttled or blocked. Most bad addresses fall into four buckets you can catch locally, for free, before sending anything.

Check Catches Cost
Syntax john@@gmail, jane@gmail,com instant
Typo domains gmial.com, hotnail.com instant
MX lookup domains that can't receive mail 1 DNS query/domain
Disposable / role mailinator.com, info@, noreply@ instant

Setup

pip install dnspython email-validator 

The script

import csv, sys from functools import lru_cache import dns.resolver from email_validator import validate_email, EmailNotValidError DISPOSABLE = {"mailinator.com", "10minutemail.com", "guerrillamail.com", "yopmail.com", "tempmail.com"} ROLE = {"info", "admin", "support", "sales", "noreply", "no-reply", "contact", "office", "webmaster"} TYPOS = {"gmial.com": "gmail.com", "gamil.com": "gmail.com", "hotnail.com": "hotmail.com", "yaho.com": "yahoo.com"} @lru_cache(maxsize=None) def has_mx(domain): try: return bool(dns.resolver.resolve(domain, "MX", lifetime=5)) except Exception: return False def check(addr): addr = addr.strip().lower() try: addr = validate_email(addr, check_deliverability=False).normalized except EmailNotValidError: return addr, "invalid_syntax" local, domain = addr.rsplit("@", 1) if domain in TYPOS: return addr, f"typo->{TYPOS[domain]}" if domain in DISPOSABLE: return addr, "disposable" if local in ROLE: return addr, "role" if not has_mx(domain): return addr, "no_mx" return addr, "ok" rows = list(csv.DictReader(open(sys.argv[1], newline=""))) seen = set() with open("cleaned.csv", "w", newline="") as f: w = csv.DictWriter(f, fieldnames=list(rows[0].keys()) + ["status"]) w.writeheader() for r in rows: email, status = check(r["email"]) if email in seen: status = "duplicate" seen.add(email) w.writerow({**r, "email": email, "status": status}) print("wrote cleaned.csv") 

Run it with python clean.py contacts.csv and filter on status == ok.

Notes

  • Cache MX lookups per domain (done above with lru_cache). 10,000 emails often share fewer than 500 domains.
  • Don't "verify" by opening SMTP connections to every mailbox; many providers treat that as abuse and may blocklist your IP.
  • Role addresses aren't invalid, just low-engagement; decide per campaign.
  • Real disposable lists have thousands of domains; refresh yours from a maintained public list.

If you want this ready-made with a maintained disposable-domain list, typo correction, parallel DNS, and a summary report, it's packaged as Email List Cleaner Pro ($9), a single CLI that runs offline except for DNS.