Tutorial · Python

Validate phone numbers in Python

The phonenumbers library for offline validation and formatting, an API call when you need carrier and line type, batches for lists and a Django form field, with code we ran.

Updated October 29, 2026 · 5 min read

To validate phone numbers in Python, use the phonenumbers library (the Python port of Google’s libphonenumber) for offline checks and formatting, and call a phone validation API when you need the carrier and line type for any country, the US included. This guide covers both, plus batch validation for lists and a Django form field. We ran every snippet with Python 3.15 and phonenumbers 9.0.42; the API calls ran against a local stub of the numlookupapi API that returns the documented responses.

Offline validation with phonenumbers

pip install phonenumbers
# phone_check.py – pip install phonenumbers
import phonenumbers
from phonenumbers import PhoneNumberFormat, PhoneNumberType, carrier, geocoder

TYPES = {
    PhoneNumberType.MOBILE: "mobile",
    PhoneNumberType.FIXED_LINE: "landline",
    PhoneNumberType.FIXED_LINE_OR_MOBILE: "landline_or_mobile",  # US and Canada
    PhoneNumberType.VOIP: "voip",
    PhoneNumberType.TOLL_FREE: "toll_free",
    PhoneNumberType.PREMIUM_RATE: "premium_rate",
}


def check(number: str, default_region: str = "US") -> dict:
    try:
        parsed = phonenumbers.parse(number, default_region)
    except phonenumbers.NumberParseException:
        return {"valid": False}  # not a phone number at all
    return {
        "valid": phonenumbers.is_valid_number(parsed),
        "e164": phonenumbers.format_number(parsed, PhoneNumberFormat.E164),
        "international": phonenumbers.format_number(parsed, PhoneNumberFormat.INTERNATIONAL),
        "type": TYPES.get(phonenumbers.number_type(parsed), "other"),
        "region": phonenumbers.region_code_for_number(parsed),
        "location": geocoder.description_for_number(parsed, "en"),
        "original_carrier": carrier.name_for_number(parsed, "en"),
    }

What it returned:

>>> check("0803 123 4567", "NG")
{'valid': True, 'e164': '+2348031234567', 'international': '+234 803 123 4567',
 'type': 'mobile', 'region': 'NG', 'location': 'Nigeria', 'original_carrier': 'MTN'}
>>> check("+1 415 858 6273")
{'valid': True, 'e164': '+14158586273', 'international': '+1 415-858-6273',
 'type': 'landline_or_mobile', 'region': 'US', 'location': 'California', 'original_carrier': ''}
>>> check("+1 555 555 5555")
{'valid': False, 'e164': '+15555555555', 'international': '+1 555-555-5555',
 'type': 'other', 'region': None, 'location': '', 'original_carrier': ''}
>>> check("call me maybe")
{'valid': False}

What the functions do:

The library runs without network access and is the right tool for format checks and normalisation. Store the E.164 form.

Carrier and line type with the API

The numlookupapi API returns carrier, line type, location, time zones and every format for any number, including US numbers. Get a free API key on the pricing page (250 requests a month for testing and personal projects, no credit card; commercial use from the Basic plan) and set it as NUMLOOKUPAPI_KEY.

# phone_api.py – Python 3.10 or later, pip install requests
import os
from urllib.parse import quote

import requests

API = "https://api.numlookupapi.com/v1/validate"

session = requests.Session()
session.headers["apikey"] = os.environ["NUMLOOKUPAPI_KEY"]


def lookup(number: str, country: str | None = None) -> dict:
    params = {"country_code": country} if country else None
    res = session.get(f"{API}/{quote(number)}", params=params, timeout=10)
    if res.status_code == 422:
        # malformed input: invalid_number (empty) or invalid_country_code
        return {"valid": False, "error": res.json().get("message")}
    res.raise_for_status()  # 401 key, 429 quota or rate limit
    return res.json()  # an unknown number is 200 with "valid": false
>>> data = lookup("+14158586273")
>>> data["valid"], data["line_type"], data["carrier"], data["e164_format"]
(True, 'mobile', 'AT&T Mobility LLC', '+14158586273')
>>> lookup("4158586273", country="US")["e164_format"]
'+14158586273'

The response has valid, country_code, location, carrier, line_type, number_type (with voip for internet-based numbers and landline_or_mobile for US numbers), timezones and the number in E.164, national, international and RFC 3966 formats. Check valid rather than the status code: a number that is not in use returns HTTP 200 with "valid": false. A 429 means the monthly quota is used up or, on the free plan, more than 10 requests a minute were sent.

Like the library, the API’s carrier names the network the number range was assigned to. For the network after porting and whether the phone is reachable right now, add hlr=1 on a paid plan (see the HLR lookup API).

Validate a list in batches

POST /v1/validate takes up to 100 numbers per request. Each number counts as one request, identical entries in a batch are validated and billed once, and a number that cannot be read gets "error": "invalid_number" in its own entry instead of failing the request:

def lookup_many(numbers: list[str], country: str | None = None) -> dict[str, dict]:
    unique = list(dict.fromkeys(numbers))  # keep the order, drop repeats
    results: dict[str, dict] = {}
    for start in range(0, len(unique), 100):  # up to 100 numbers per request
        body = {"numbers": unique[start : start + 100]}
        if country:
            body["country_code"] = country
        res = session.post(API, json=body, timeout=30)
        res.raise_for_status()
        results.update(res.json()["data"])  # keyed by the number as sent
    return results

country_code applies to every number without + or 00. The same number written two ways counts twice, so normalise with check() first and send the e164 values. For a CSV of contacts:

import csv

with open("contacts.csv", newline="") as f:
    phones = [row["phone"] for row in csv.DictReader(f)]
results = lookup_many(phones, country="US")
invalid = [number for number, row in results.items() if not row["valid"]]

Without code, the bulk phone validator checks a list in your browser, and the batch upload in the numlookupapi dashboard takes CSV, TXT and Excel files.

Django form field

Validate and normalise in the form, so every stored number is E.164:

from django import forms
import phonenumbers


class SignupForm(forms.Form):
    phone = forms.CharField(max_length=32)

    def clean_phone(self):
        raw = self.cleaned_data["phone"]
        try:
            parsed = phonenumbers.parse(raw, "US")  # default region for numbers without +
        except phonenumbers.NumberParseException:
            raise forms.ValidationError("Enter a valid phone number.")
        if not phonenumbers.is_valid_number(parsed):
            raise forms.ValidationError("Enter a valid phone number.")
        return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)

With Django 6.1, (415) 858-6273 became +14158586273, +44 20 7946 0958 became +442079460958, and 555-555-5555 and hello got the error. If you need the carrier or VoIP detection at sign-up, call lookup() after the form is valid, ideally in a background task (Celery, RQ or Django’s task framework), and store line_type and number_type next to the number. The django-phonenumber-field package wraps the same library as a model and form field if you prefer a ready-made field.

More languages: phone number validation in JavaScript and in PHP. For the format itself, see E.164 explained.

Frequently asked questions

How do I validate a phone number in Python?

Install phonenumbers, parse the number with phonenumbers.parse(number, 'US') and call phonenumbers.is_valid_number(parsed). It checks length and prefixes against Google's numbering-plan data. parse() raises NumberParseException for input that is not a phone number at all.

How do I get the carrier of a phone number in Python?

phonenumbers.carrier.name_for_number(parsed, 'en') returns the network a number block was originally assigned to, and an empty string for US and Canadian numbers. For carrier and line type that also cover US numbers, call a lookup API such as numlookupapi. Both name the network that issued the number; only an HLR lookup shows the network after porting.

How do I format a phone number to E.164 in Python?

phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164) returns '+14158586273' for a US number. Store that form and format nationally only for display.

Why does phonenumbers say a US mobile number is not MOBILE?

In the US and Canada, mobile and landline numbers share the same number ranges, so number_type() returns FIXED_LINE_OR_MOBILE. Only a carrier lookup can tell them apart.

Phone number API

Validate numbers like this in your app

numlookupapi checks any phone number worldwide in one request and returns validity, line type, carrier, location and every standard format as JSON.

Read the documentation

250 free requests every month. No credit card required.

GET https://api.numlookupapi.com/v1/validate/+12069220880

{
  "valid": true,
  "number": "12069220880",
  "local_format": "2069220880",
  "international_format": "+12069220880",
  "country_prefix": "+1",
  "country_code": "US",
  "country_name": "United States of America",
  "location": "Seattle",
  "line_type": "landline",
  "e164_format": "+12069220880",
  "national_format": "(206) 922-0880"
}

Keep reading

Start using our phone number API for free today!

Get 250 requests / month for free