# How to validate an email address in Python

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:

```python
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:

| Address | Passes the regex | Problem |
| --- | --- | --- |
| `jane@gmial.com` | yes | The domain is a typo and does not exist |
| `former.employee@example.com` | yes | The mailbox was closed |
| `jane@example-site.com` | yes | The domain has no MX record, so it receives no mail |
| `x7k2@temp-inbox.example` | yes | Disposable 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.

```python
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:

```json
{
  "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:

```python
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
```

- **accept**: the mail server accepted the mailbox and no risk signal advises against it.
- **reject**: the address cannot receive mail. See the [reason](https://www.easyemailverification.com/en-US/help/result-codes) for details (`rejected_email`, `invalid_domain`, `no_mx_record`…).
- **suggest**: `did_you_mean` has a corrected address, for example `typo@gmail.com` for `typo@gmial.com`. Show it to the user instead of rejecting.
- **review**: an [unknown result](https://www.easyemailverification.com/en-US/help/unknown) (the server timed out or greylisted the check), a [catch-all domain](https://www.easyemailverification.com/en-US/help/accept-all) or a [disposable address](https://www.easyemailverification.com/en-US/help/disposable). Do not treat unknown as invalid: accept the signup and confirm the address by email, or verify it again later.

## 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:

```python
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](https://www.easyemailverification.com/en-US/bulk-email-verification) and the [API reference](https://www.easyemailverification.com/en-US/api/reference).

## Test addresses

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

| Address | Answer |
| --- | --- |
| `valid@sandbox.easyemailverification.com` | `valid`, `accepted_email` |
| `invalid@sandbox.easyemailverification.com` | `invalid`, `rejected_email` |
| `unknown@sandbox.easyemailverification.com` | `unknown`, `timeout` |
| `catchall@sandbox.easyemailverification.com` | `valid`, `accept_all: true`, `safe_to_send: false` |
| `disposable@sandbox.easyemailverification.com` | `valid`, `disposable: true`, `safe_to_send: false` |
| `typo@gmial.com` | `invalid`, `did_you_mean: typo@gmail.com` |
| `quota@sandbox.easyemailverification.com` | HTTP `402` (no credits) |

When it works, create a free account, generate a key under [API settings](https://dashboard.easyemailverification.com/apisettings) and set it in `EEV_API_KEY`. Each verified address uses one credit; the free plan includes 50 verifications a day.

## Next steps

- Prefer a library? The official package wraps everything above (single, batch, bulk, credits, sandbox): `pip install easyemailverification` ([PyPI](https://pypi.org/project/easyemailverification/)).
- [Email Verification API](https://www.easyemailverification.com/en-US/api): plans, limits and the other endpoints.
- [Result codes](https://www.easyemailverification.com/en-US/help/result-codes): every `result`, `reason` and risk signal.
- [Examples in other languages](https://github.com/EasyEmailVerification/api-examples) on GitHub.
