← Blog

Validate an email address in Python: regex, stdlib, and API

Four layers, from a one-line regex to a live API call — and a clear-eyed account of exactly what each one proves and what it can't.

Contents

Someone hands you a signup form and asks you to "validate the email." You reach for a regex, ship it, and move on. Then the bounces start, the disposable accounts pile up, and you learn the hard way that "looks like an email" and "can receive mail" are two very different claims.

Here's how to check an email address in Python, layer by layer — with an honest account of what each layer actually proves. Because the mistake isn't using a regex; it's not knowing what your check does and doesn't cover.

Layer 1: the regex (format only)

This is what most people mean by "validate an email," and for a lot of cases it's the right amount of effort:

import re

EMAIL_RE = re.compile(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")

def looks_like_email(value: str) -> bool:
    return bool(EMAIL_RE.match(value.strip()))

You'll find people insisting you need the "full" RFC 5322 regex instead. Don't. It's a famous multi-thousand-character monster, it still accepts addresses that will never receive mail, and it rejects some that would. A short, strict-enough pattern plus a length cap (addresses max out at 254 characters) catches the fat-finger typos, and that's all a regex can honestly promise.

What this proves: the string is shaped like an address. That's it. test@nonexistent-domain-9f2b.com passes this every time and bounces every time. The regex has no idea whether the domain exists, let alone the mailbox.

Layer 2: the standard-library trap

A tempting move is to reach into Python's email module:

from email.utils import parseaddr

name, addr = parseaddr("Sarah <sarah@acme.com>")
# addr == "sarah@acme.com"

parseaddr is genuinely useful — but it is not a validator. It parses an address out of a header; hand it pure garbage and it will happily hand a chunk of that garbage back to you without complaint. If you're using parseaddr as your validation, you don't have validation. Use it to extract, then validate what you extracted.

Layer 3: a real library, and a live MX check

When the regex isn't enough, don't hand-roll RFC parsing — use a maintained library. The email-validator package does correct syntax validation, normalizes the address, and can do a live deliverability check:

from email_validator import validate_email, EmailNotValidError

try:
    info = validate_email("you@example.com", check_deliverability=True)
    normalized = info.normalized   # canonical form, safe to store
except EmailNotValidError as err:
    print("rejected:", err)

With check_deliverability=True, it resolves the domain's mail servers — the single most valuable check past syntax. No MX (or A) record means no mailbox can receive mail there, whatever the syntax says. If you'd rather do just that step yourself, dnspython 2.x is one call:

import dns.resolver

def has_mail_servers(domain: str) -> bool:
    try:
        return len(dns.resolver.resolve(domain, "MX")) > 0
    except dns.resolver.NoAnswer:
        # some domains accept mail on their A record with no MX
        return bool(dns.resolver.resolve(domain, "A"))
    except Exception:
        return False

What this proves: the domain can receive mail. A big step up. What it still doesn't tell you: whether the specific mailbox exists, whether the domain is a disposable burner, or whether it's a shared role alias — three things that matter enormously at a signup form and not at all to a DNS resolver.

Layer 4: the checks local code can't do

You can't tell from Python alone that x7f2@mailinator.com is a throwaway inbox, or that admin@acme.com is a shared alias rather than a person. Those need lists and lookups maintained somewhere off your box — which is where a validation API earns its place:

import requests

r = requests.post(
    "https://api.boundstone.io/v1/verify/email",
    headers={"authorization": "Bearer bs_live_YOUR_KEY"},
    json={"email": "you@example.com"},
    timeout=5,
)
data = r.json()

print(data["valid_syntax"], data["mx_found"], data["disposable"], data["role_account"])

One call gives you syntax, a live MX lookup, disposable-domain detection, role-account and free-provider flags. If you just want to try it without writing any code, the free email validator runs the exact same checks in the browser.

The part most vendors skip

Here's the thing to internalize, and it's the whole reason we built Boundstone the way we did: no email check — not the regex, not the library, not the API — can prove a mailbox exists without a live SMTP probe, and SMTP probes are unreliable enough that a "yes" from one often means nothing. A large share of mail servers accept-then-bounce, or answer positively to every address (a "catch-all").

So the honest thing an API can do is tell you what it actually checked. Every Boundstone response carries that, explicitly:

print("checked:    ", data["checks"]["performed"])
# ['syntax', 'mx', 'disposable_list', 'role_list']

print("not checked:", data["checks"]["not_performed"])
# ['smtp_mailbox', 'catch_all']

That second list is the point. A validator that returns a confident valid: true and won't tell you whether that includes a mailbox check is asking you to trust a word. One that hands you not_performed: ["smtp_mailbox", "catch_all"] is telling you exactly how far to trust the verdict — which is the only kind of trust worth anything.

Which layer do you actually need?

Layer Proves Cost Reach for it when
Regex Format only Instant, free Catching typos in a form field
email-validator Format + MX A dependency, a DNS call You control the code path and want no external service
API Format + MX + disposable + role One HTTP call Signup screening, list hygiene, anything user-facing

Start at the top and go down only as far as the problem needs. A newsletter field is fine with a regex. A paid-signup form that fraudsters probe with disposable inboxes wants Layer 4. The failure mode isn't picking the wrong layer — it's shipping a check and forgetting what it doesn't cover.

Whatever you choose, store the normalized form, handle the "we didn't check that" cases explicitly, and never let a green checkmark stand in for a claim nobody actually verified. The full API reference — every field, every not_performed case — is in the docs.

Frequently asked questions

Can a regex fully validate an email address in Python?

A regex can confirm an address is well-formed, meaning the right shape of local part, @ symbol, and domain, but that is the limit of what it proves. It cannot tell you whether the domain accepts mail or whether the mailbox actually exists, and overly strict patterns often reject legitimate addresses such as plus-tags, subdomains, or long TLDs. Treat regex as a cheap first syntax gate, then layer a DNS or API check on top when you need more confidence than shape alone.

How do I check if an email's domain can actually receive mail in Python?

You resolve the domain's published MX records, either with a DNS library in Python or by calling an API that does the lookup for you. An MX check confirms the domain is set up to receive email, which rules out typo'd or dead domains, but it does not confirm that a specific mailbox exists. Boundstone's email endpoint runs exactly this layer, syntax plus MX plus disposable-list and role-address checks, and returns an mx_found field, while honestly reporting SMTP mailbox verification and catch-all detection as not performed.

What does a 'valid' email result from a validation API actually prove?

It depends entirely on which checks ran, which is why an honest tool lists them rather than giving one opaque verdict. Boundstone's email response marks syntax, MX, disposable_list, and role_list as performed, so a valid address is well-formed, has a mail-accepting domain, and is not a throwaway or role address like info@, and it also flags whether the address is a free provider. It does not prove the individual mailbox is real, because SMTP mailbox and catch-all detection are reported as not performed, so you can gate signups on the result without treating it as a deliverability guarantee it never made.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist