To validate an email address in Django you run three checks, not one — and the framework only ships the first. Django's validators confirm an address is shaped correctly; they never ask whether its domain can receive mail or whether the mailbox exists. Real validation is layered: syntax, then a DNS lookup, then an SMTP mailbox probe. This guide builds each layer as a Django validator you can drop into a form or a Django REST Framework serializer, and shows where a verification API takes over. It picks up where the Python guide leaves off, so the DNS and SMTP reasoning there carries over intact.
The short answer#
Use validate_email (or any EmailField) for syntax, dnspython for the MX
lookup, and a verification API for the SMTP mailbox check — wired as three
validators, cheapest first, short-circuiting as soon as one is decisive. Don't
open SMTP connections from your Django process to probe mailboxes: outbound port
25 is blocked on most hosts, and the answer depends on sending-IP reputation and
greylisting you don't want to reimplement. Each layer rules addresses out more
cheaply than the last; only the API can rule one in. If the pipeline is new to
you, what email verification is covers the
terms first.
Layer 1: format validation with Django's validators#
Django already owns layer one. django.core.validators.validate_email is an
EmailValidator instance that raises ValidationError on a malformed address and
returns None on a good one:
from django.core.validators import validate_email
from django.core.exceptions import ValidationError
def is_valid_syntax(email: str) -> bool:
try:
validate_email(email)
return True
except ValidationError:
return False
You rarely call it by hand. Every forms.EmailField, models.EmailField, and
DRF serializers.EmailField attaches EmailValidator automatically, so declaring
a field gives you syntax validation for free:
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # EmailValidator runs during clean()
Know exactly what this buys you. EmailValidator checks shape against an
RFC-derived grammar — it never resolves DNS, never opens a socket, and has no idea
whether example.com exists. definitely-fake@gmail.com passes.
info@company-that-folded.com passes. typo@gmial.com passes. All three are
undeliverable, and no validator that only reads the string will ever tell you so,
for the same reason regex email validation fails:
syntax and deliverability are different questions. One is a fact about the string;
the other is a fact about the internet.
Layer 2: does the domain accept mail?#
This is the first layer Django doesn't hand you, and it's cheap to bolt on. A
domain with no MX records can't accept mail for anyone, so one DNS lookup
eliminates dead domains, misspelled company names, and invented TLDs. Django has
no built-in MX resolver, so reach for dnspython and wrap it in a plain function:
import dns.resolver # pip install dnspython
def has_mail_route(domain: str) -> bool:
try:
return len(dns.resolver.resolve(domain, "MX")) > 0
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers):
return False
A Django validator is just a callable that raises ValidationError on bad input,
so promote the check into one and it plugs straight into any field's validators
list:
from django.core.exceptions import ValidationError
def validate_deliverable_domain(email: str) -> None:
domain = email.rsplit("@", 1)[-1]
if not has_mail_route(domain):
raise ValidationError("This domain can't receive email.")
validate_deliverable_domain("jane@gmail.com") # passes
validate_deliverable_domain("jane@company-that-folded.com") # raises ValidationError
If you want to honour domains that accept mail on an A record with no MX, fall
back to resolving A/AAAA when the MX set is empty — but an MX check covers the
vast majority of real addresses.
Layer 3: does the mailbox actually exist?#
Layers one and two can only rule an address out. A domain can publish perfect MX
records and still have no mailbox at the address you're holding —
noreply-9f2x@gmail.com is valid syntax on a live mail route, and it's still a
mailbox that was never created. Confirming a specific mailbox means the SMTP
delivery conversation: connect to the mail host, issue RCPT TO, read the reply,
and disconnect before sending anything.
In principle you could script that from Django with smtplib. In practice you
shouldn't run it from your application server: most cloud hosts block outbound
port 25, the answer depends on the reputation of the IP you connect from, and
receiving servers greylist and rate-limit unfamiliar senders — so a probe that
works in a local test quietly fails, or gets you blocklisted, in production.
How email verification works walks the full
pipeline, catch-all domains and all. This is the layer worth delegating.
Qualisend's verify endpoint runs the whole pipeline — syntax, DNS, and the SMTP
mailbox probe — from reputation-managed infrastructure and returns a verdict. Call
it from a validator with requests, reading your key from the environment:
import os
import requests # pip install requests
QUALISEND_VERIFY_URL = "https://api.qualisend.com/v1/verify"
def verify(email: str) -> dict:
res = requests.post(
QUALISEND_VERIFY_URL,
headers={"Authorization": f"Bearer {os.environ['QUALISEND_API_KEY']}"},
json={"email": email},
timeout=10,
)
res.raise_for_status()
return res.json()["result"] # {"status", "score", "reason", "sub_flags"}
The response is a { "result": { ... } } envelope. Read result["status"] —
deliverable, risky, undeliverable, or unknown — alongside a score, a
reason, and sub_flags for traits like role or disposable addresses. Check the
API reference for the exact field shapes; here you only need the
status to raise on an address that won't deliver:
def validate_mailbox(email: str) -> None:
if verify(email)["status"] == "undeliverable":
raise ValidationError("We couldn't confirm a mailbox at this address.")
You might also raise on risky, or store the score and let it through — that's
a policy call. Rejecting undeliverable is the safe default; the
serverless signup guide covers how
strict to be at the point of collection.
Putting the layers together to validate an email address in Django#
The idiomatic place to compose the three layers on a form is a clean_<field>
method. Django calls clean_email only after the field's own EmailValidator
has passed, so syntax is already handled; you add the domain and mailbox checks in
order, each raising early:
from django import forms
class SignupForm(forms.Form):
email = forms.EmailField() # layer 1 for free
def clean_email(self):
email = self.cleaned_data["email"] # syntax already passed
validate_deliverable_domain(email) # layer 2 — cheap, local
validate_mailbox(email) # layer 3 — the API round-trip
return email
That ordering is the whole trick: the field's EmailValidator short-circuits junk
before your code runs, the local DNS check rules out dead domains for nothing, and
the API is hit only for addresses that cleared both. Wrong-order validation — or
skipping the cheap layers — spends a credit on every typo.
One thing to decide up front: what happens when the API call itself fails. A
network timeout or a requests exception inside clean_email shouldn't hand a
real customer a validation error they can't fix. Catch the request failure
separately from ValidationError, and treat an unreachable service as unknown
rather than undeliverable — let the signup through and re-verify the address
later, instead of blocking registration on a transient outage. The syntax and MX
layers already ran locally, so you're only softening the layer that depends on the
network.
Because both custom validators raise Django's ValidationError, the exact same
two functions drop into a Django REST Framework serializer unchanged. DRF's
validate_<field> hook catches that exception and turns it into a clean 400:
from rest_framework import serializers
class SignupSerializer(serializers.Serializer):
email = serializers.EmailField() # layer 1 for free
def validate_email(self, value):
validate_deliverable_domain(value) # layer 2
validate_mailbox(value) # layer 3
return value
This is the same three-layer shape you'll find in the Node.js and Python versions of this guide — the layering, not the framework, is what makes validation work. When you're choosing which verification service backs layer three, the API comparison lines up the options.
Frequently asked questions#
Is Django's EmailValidator enough to validate an email address?#
For syntax, yes — validate_email (and every EmailField that uses it) is the
right layer-one check and better than a hand-rolled regex. But it validates
shape, not deliverability: it never resolves DNS or contacts a mail server, so a
pass means "looks like an email," not "will deliver." Pair it with an MX lookup and
an SMTP mailbox check before you trust the address.
How do I write a custom email validator in Django?#
A validator is any callable that takes the value and raises
django.core.exceptions.ValidationError on failure. Define a function like
validate_deliverable_domain(email) that raises when there's a problem and returns
None otherwise, then pass it in the field's validators=[...] list or call it
from a form's clean_email method. The same callable works in a DRF serializer's
validate_email hook, because DRF also catches Django's ValidationError.
Can I check whether a mailbox exists from Django without an API?#
Partly. dnspython confirms the domain accepts mail, which rules out dead domains
for free and needs nothing beyond a small dependency. Confirming the mailbox
means an SMTP conversation you can attempt with smtplib but shouldn't run from
your app server — port 25 is widely blocked and the result depends on your IP's
reputation. That's the layer a verification service exists to handle.
Should I run these checks at signup or when cleaning a list?#
Both, at different depths. Run the syntax and MX layers synchronously in
clean_email — they're fast enough to block the request and give instant feedback
— and act on the API verdict there too. Reserve heavier batch verification for
list cleaning and back-office work, where latency doesn't matter and you can
process addresses in bulk.
Ready to add the SMTP layer? Drop a syntactically perfect address into the
free email checker to watch an EmailField-approved string
come back undeliverable, then wire the same verdict into your forms with the
API reference — copy-paste examples included.