If you've ever inherited a marketing database, you know the state it's usually in: years of form submissions, old CSV imports, a few thousand test@test.com-style entries, and a bounce rate that keeps climbing every quarter.
Cleaning that up by hand isn't realistic. What you actually want is a script that takes a CSV of emails and hands you back a clean list — valid, invalid, disposable, and risky, clearly separated. Here's how to build that in Python.
The problem with an unverified list
Every invalid or disposable address on your list costs you in three ways:
Bounce rate — too many hard bounces and your sending domain's reputation drops, which hurts deliverability for every email after that, not just the bad ones
Wasted spend — you're paying for sends (and sometimes per-contact pricing in your ESP) on addresses that were never going to convert
Bad reporting — open/click rates look worse than they should because a chunk of your list can never even receive the email
Bulk verification fixes all three in one pass.
Script: validate a CSV of emails
This example reads a CSV of emails, calls a bulk validation API, and writes out three separate files — valid, invalid, and risky — so you can decide what to do with each bucket.
pythonimport csv
import requests
import time
API_KEY = "your_api_key_here"
INPUT_FILE = "leads.csv"
BATCH_SIZE = 50 # keep requests reasonably sized
def read_emails(filepath):
with open(filepath, newline='') as f:
reader = csv.DictReader(f)
return [row['email'] for row in reader if row.get('email')]
def validate_email(email):
response = requests.get(
'https://gamalogic.com/emailvrf/',
params={'emailid': email, 'apikey': API_KEY}
)
return response.json()
def validate_batch(emails):
results = {'valid': [], 'invalid': [], 'risky': []}
for email in emails:
try:
result = validate_email(email)
except requests.RequestException as e:
print(f"Error validating {email}: {e}")
continue
if not result.get('valid'):
results['invalid'].append(email)
elif result.get('disposable') or result.get('catchall'):
results['risky'].append(email)
else:
results['valid'].append(email)
time.sleep(0.05) # basic rate limiting, adjust to your plan's limits
return results
def write_results(results):
for category, emails in results.items():
with open(f'{category}_emails.csv', 'w', newline='') as f:
writer = csv.writer(f)
writer.writerow(['email'])
for email in emails:
writer.writerow([email])
if name == 'main':
emails = read_emails(INPUT_FILE)
print(f"Validating {len(emails)} emails...")
results = validate_batch(emails)
write_results(results)
print(f"Valid: {len(results['valid'])} | "
f"Invalid: {len(results['invalid'])} | "
f"Risky: {len(results['risky'])}")
Speeding this up for larger lists
The script above validates one email at a time, which is fine for a few hundred addresses but slow for 10K+. Two ways to speed it up:
Use the bulk endpoint if your provider has one. Rather than one HTTP call per email, services like Gamalogic email validation support bulk upload endpoints (CSV/JSON) that can process large lists in a single job — their bulk pipeline handles up to 100,000 emails per hour, which is a very different shape of problem than a per-row loop.
Parallelize with a thread pool if you're staying on single-email calls:
pythonfrom concurrent.futures import ThreadPoolExecutor
def validate_batch_parallel(emails, max_workers=10):
results = {'valid': [], 'invalid': [], 'risky': []}
def process(email):
try:
return email, validate_email(email)
except requests.RequestException:
return email, None
with ThreadPoolExecutor(max_workers=max_workers) as executor:
for email, result in executor.map(process, emails):
if result is None:
continue
if not result.get('valid'):
results['invalid'].append(email)
elif result.get('disposable') or result.get('catchall'):
results['risky'].append(email)
else:
results['valid'].append(email)
return results
Keep max_workers reasonable — most APIs have a rate limit, and hammering it with 100 concurrent threads just gets you throttled.
What to do with the "risky" bucket
Don't just delete catch-all or ambiguous addresses outright — they're not necessarily bad, just unconfirmed. Common approaches:
Send them a low-stakes re-engagement email first before including them in a big campaign
Segment them separately in your ESP so a spike in bounces there doesn't affect your primary sending domain's reputation
Re-validate them periodically — catch-all detection improves over time as providers refine their SMTP-tarpit handling
Wrapping up
A one-time cleanup script like this is worth running before any major campaign, CRM migration, or list import. If you want to test it against your own data, Gamalogic gives 500 free credits to start, and their bulk validation docs cover the batch endpoint if you want to skip the per-row loop entirely.
If you're doing this at real scale (millions of rows), I'd love to hear how you're handling rate limits and cost — feels like there's always a tradeoff between speed and API cost at that volume.
Top comments (0)