Validate phone numbers in Django forms
Phone number validation in Django that knows the national numbering plans: what people type is read in the plans you pick, a number no plan accepts gets Django's usual error, and what you store is its E.164 form. The reading is done by one file with no dependency, from the Python guide; the rest is Django's own validation hook, a few lines long.
1. The file
Take phone.py from the Python guide, with the plans of the countries you serve (the link opens it for the United Kingdom, the United States and France, the plans the tests below use). It needs Python 3.9 or later; standard library only. Its parse reads a number as typed and returns the number with its E.164 form, region and line type, or None when no plan accepts it.
pip install django2. The Django side
fields.py
The field: CharField, with the language guide's parse in to_python.
"""A Django form field for phone numbers, read by phone.py in its plans."""
from django import forms
from django.core.exceptions import ValidationError
import phone
class PhoneNumberField(forms.CharField):
"""Reads what was typed (national numbers in default_region) and cleans to the E.164 form."""
default_error_messages = {"invalid": "Enter a valid phone number."}
def __init__(self, *, default_region=None, **kwargs):
self.default_region = default_region
kwargs.setdefault("max_length", 40)
super().__init__(**kwargs)
def to_python(self, value):
text = super().to_python(value)
if text in self.empty_values:
return text
number = phone.parse(text, self.default_region)
if number is None:
raise ValidationError(self.error_messages["invalid"], code="invalid")
return number.e164forms.py
A form that uses it.
from django import forms
from fields import PhoneNumberField
class SignupForm(forms.Form):
name = forms.CharField()
phone = PhoneNumberField(default_region="GB")3. The tests
test_forms.py: The tests, with unittest and a minimal Django setup.
import unittest
import django
from django.conf import settings
settings.configure(USE_I18N=False)
django.setup()
from forms import SignupForm # noqa: E402
class SignupFormTest(unittest.TestCase):
def test_cleans_a_national_number_to_e164(self):
form = SignupForm({"name": "Ada", "phone": "07400 123456"})
self.assertTrue(form.is_valid(), form.errors)
self.assertEqual(form.cleaned_data["phone"], "+447400123456")
def test_reads_an_international_number(self):
form = SignupForm({"name": "Ada", "phone": "+1 201-555-0123"})
self.assertTrue(form.is_valid(), form.errors)
self.assertEqual(form.cleaned_data["phone"], "+12015550123")
def test_shows_the_error_on_the_field(self):
form = SignupForm({"name": "Ada", "phone": "+44 7400 12345"})
self.assertFalse(form.is_valid())
self.assertEqual(form.errors["phone"], ["Enter a valid phone number."])
def test_still_required(self):
form = SignupForm({"name": "Ada", "phone": ""})
self.assertEqual(form.errors["phone"], ["This field is required."])
if __name__ == "__main__":
unittest.main()python -m unittestRun before publishing with Django 6.1.2 and the file generated for GB, US, FR, as 2026-10-25.
What is specific to Django
to_pythonis where a field turns input into a value: raisingValidationErrorthere puts the message on the field, andrequired,max_lengthand your validators then run on the E.164 form.- Store it in a
models.CharField(max_length=16, unique=True):form.cleaned_data["phone"]is already E.164 (see storing numbers in a database). - A
ModelFormtakes the field the same way: declarephone = PhoneNumberField(default_region="GB")on the form to replace the model field's default.