DEV Community

Cover image for Bulk Email Verification in Python: Cleaning 10,000 Emails in One Script
SREERAG P
SREERAG P

Posted on

Bulk Email Verification in Python: Cleaning 10,000 Emails in One Script

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)