How to validate an email address in Python

Create a free accountPlans and credits

Validating an email address in Python takes two steps. A regular expression catches addresses that are badly written. Only a check against the receiving mail server can tell you whether the mailbox exists. This guide does both, with tested code: a small regex, then the Easy Email Verification API through requests. You can try every example with the free sandbox key eev_sandbox_key, without an account.

1. Check the format with a regular expression

Keep the pattern simple. It should reject obvious mistakes (no @, spaces, no dot in the domain) and leave everything else to the mailbox check:

import re

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


def looks_like_email(address: str) -> bool:
    """Cheap first filter: something@something.something, no spaces."""
    return bool(EMAIL_FORMAT.match(address))


looks_like_email("jane@example.com")      # True
looks_like_email("jane.doe@example")      # False: no dot in the domain
looks_like_email("jane doe@example.com")  # False: space

Avoid the long "RFC 5322" patterns found online. They are hard to read, they still accept addresses that cannot receive mail, and some of them reject valid addresses such as jane+news@example.com.

Why a regex is not enough

A regex only proves that the text has the shape of an address. All of these pass the format check and still bounce:

AddressPasses the regexProblem
jane@gmial.comyesThe domain is a typo and does not exist
former.employee@example.comyesThe mailbox was closed
jane@example-site.comyesThe domain has no MX record, so it receives no mail
x7k2@temp-inbox.exampleyesDisposable inbox that disappears in a few minutes

To catch them you need DNS (does the domain exist and have an MX record?) and an SMTP conversation with the mail server (does it accept this mailbox?). Many networks block outgoing connections on port 25, and mail servers treat repeated checks from unknown hosts as abuse, so doing it from your own server is unreliable. An email verification API does those checks for you, without sending any email.

2. Verify the mailbox with the API

Install requests (pip install requests) and call GET /v1/verify. Keep the API key on the server, in the EEV_API_KEY environment variable; without it, the code below uses the sandbox key.

import os

import requests

API_URL = "https://api.easyemailverification.com/v1/verify"
API_KEY = os.environ.get("EEV_API_KEY", "eev_sandbox_key")


def verify_email(address: str) -> dict:
    response = requests.get(
        API_URL,
        params={"email": address},  # requests URL-encodes it (+ becomes %2B)
        headers={"X-API-Key": API_KEY},
        timeout=30,
    )
    data = response.json()
    if response.status_code != 200:
        raise RuntimeError(f"EEV error {response.status_code}: {data.get('message')}")
    return data


print(verify_email("valid@sandbox.easyemailverification.com"))

The answer for that sandbox address:

{
  "email": "valid@sandbox.easyemailverification.com",
  "result": "valid",
  "reason": "accepted_email",
  "disposable": false,
  "accept_all": false,
  "role": false,
  "free": false,
  "user": "valid",
  "domain": "sandbox.easyemailverification.com",
  "mx_record": "mx.sandbox.easyemailverification.com",
  "mx_domain": "easyemailverification.com",
  "safe_to_send": true,
  "did_you_mean": "",
  "success": true,
  "message": "Sandbox response: no credits used",
  "http_code": "200"
}

Errors come back with an HTTP status and a message: 401 for an unknown key, 402 when the account has no credits left. success: true only means the request was processed; it does not mean the address is deliverable.

3. Decide what to do with the result

result is valid, invalid or unknown. Turn it into a decision your application understands:

def decide(result: dict) -> str:
    if result["did_you_mean"]:
        return "suggest"  # ask the user: did you mean ...?
    if result["result"] == "valid" and result["safe_to_send"]:
        return "accept"
    if result["result"] == "invalid":
        return "reject"
    return "review"  # unknown, catch-all or disposable: your policy decides

4. Verify up to 50 addresses in one request

POST /v1/verify takes a list of up to 50 addresses and returns one result per address, in the same format:

def verify_emails(addresses: list[str]) -> list[dict]:
    response = requests.post(
        API_URL,
        json={"emails": addresses},  # up to 50 per request
        headers={"X-API-Key": API_KEY},
        timeout=120,
    )
    data = response.json()
    if response.status_code != 200:
        raise RuntimeError(f"EEV error {response.status_code}: {data.get('message')}")
    return data


for r in verify_emails(["valid@sandbox.easyemailverification.com", "typo@gmial.com"]):
    print(r["email"], decide(r))

For whole files (thousands of addresses), use the bulk endpoints or upload the list in the dashboard instead of looping over this call: see bulk email verification and the API reference.

Test addresses

With the key eev_sandbox_key, these addresses return fixed answers and use no credits:

AddressAnswer
valid@sandbox.easyemailverification.comvalid, accepted_email
invalid@sandbox.easyemailverification.cominvalid, rejected_email
unknown@sandbox.easyemailverification.comunknown, timeout
catchall@sandbox.easyemailverification.comvalid, accept_all: true, safe_to_send: false
disposable@sandbox.easyemailverification.comvalid, disposable: true, safe_to_send: false
typo@gmial.cominvalid, did_you_mean: typo@gmail.com
quota@sandbox.easyemailverification.comHTTP 402 (no credits)

When it works, create a free account, generate a key under API settings and set it in EEV_API_KEY. Each verified address uses one credit; the free plan includes 50 verifications a day.

Next steps

By using this website, you automatically accept that we use cookies.