Guides /

Django

PY form clean_email

Checks the address in the form's clean_email, so a bad one shows up as an ordinary field error.

The code

1 Keep the key on the server

MAILVET_KEY=mv_live_your_key

Set it wherever your server reads its environment. Never put it in client code.

2 Install requests

pip install requests

3 Put the check in one place

accounts/mailvet.py

import logging
import os

import requests

log = logging.getLogger(__name__)


def check_email(email):
    # Returns MailVet's answer, or None if there is no answer.
    try:
        r = requests.get(
            "https://mailvet.dev/v1/check",
            params={"email": email},
            headers={"Authorization": f"Bearer {os.environ['MAILVET_KEY']}"},
            timeout=5,
        )
        r.raise_for_status()
        return r.json()
    except (requests.RequestException, KeyError, ValueError) as err:
        log.warning("mailvet: no answer: %s", err)
        return None

4 Use it in the form

accounts/forms.py

from django import forms
from django.core.exceptions import ValidationError

from .mailvet import check_email


class SignupForm(forms.Form):
    email = forms.EmailField()

    def clean_email(self):
        email = self.cleaned_data["email"]
        check = check_email(email)
        if check is None:
            return email  # a signup shouldn't die because of us

        if check["did_you_mean"]:
            local = email.rpartition("@")[0]
            raise ValidationError(
                f"Did you mean {local}@{check['did_you_mean']}?",
                code="typo",
            )
        if check["verdict"] == "invalid":
            raise ValidationError("That address can't receive mail.", code="invalid")
        if check["verdict"] == "risky" and check["disposable"]:
            raise ValidationError(
                "Please use a permanent address, not a throwaway one.",
                code="disposable",
            )
        return email

What to do with the answer

did_you_mean, invalid and disposable raise ValidationError, which lands on the email field. Role addresses and a risky from a DNS failure return the address unchanged.

  • valid Let it through.
  • risky Read disposable. If it’s true, reject. If false, it’s a role address or DNS couldn’t be reached, so let it through.
  • invalid Bad format, or the domain has no mail server. Reject.

did_you_mean is empty or holds a domain, such as gmail.com for gamil.com. It doesn’t change the verdict. A known typo can come back valid because squatters give those domains a mail server, so check it first. See the docs for every field.

If MailVet is down, the signup goes through

check_email catches network errors, timeouts, non-200 answers, a missing key and bad JSON, logs them and returns None. clean_email then returns the address and the form stays valid.

A 401 or a 429 takes the same path, so a bad key or a used-up quota means addresses pass unchecked. Watch your logs for it.

Watch out

  • Django skips clean_email when the EmailField itself failed, so a malformed address doesn't spend a check.
  • Use this form for signup only. A form you also use for profile edits spends a check per save.

Sources

Compared with these on November 5, 2026. If they’ve changed since, trust them over this page.

Other guides

Get API key