← Blog

How to validate a phone number in Python

The phonenumbers library validates and normalizes phone numbers offline for free — here's how, and the honest boundary where an API earns its place.

Contents

You searched "validate phone number in Python" because a form field, a signup flow, or a CSV of leads handed you strings that may or may not be real phone numbers. Good news up front: for most of what you need, Python already has the answer, and it costs nothing. The phonenumbers package — the maintained port of Google's libphonenumber — parses, validates, and normalizes numbers entirely offline. Reach for a network call only when you actually need something local data can't tell you.

Start with phonenumbers, not a network call

phonenumbers is the same rules engine that ships inside Android's dialer, repackaged for Python. Install it and you get every country's numbering plan locally:

pip install phonenumbers

Parsing takes a string and a default region. If the number is already in E.164 form — a leading + and country code — pass None for the region; the + tells the parser everything it needs:

import phonenumbers

number = phonenumbers.parse("+16504472983", None)

If your input is a national format without the +, give parse the region to interpret it against:

number = phonenumbers.parse("(650) 447-2983", "US")

Validate and format to E.164

Two calls do the work most people mean by "validate a phone number in Python." is_valid_number checks the parsed number against the region's actual numbering rules — length, prefix, allocated ranges — not just whether it looks phone-shaped. format_number with PhoneNumberFormat.E164 gives you the one canonical string worth storing:

import phonenumbers
from phonenumbers import PhoneNumberFormat

number = phonenumbers.parse("+16504472983", None)

phonenumbers.is_valid_number(number)                        # True
phonenumbers.format_number(number, PhoneNumberFormat.E164)  # "+16504472983"

Wrap it so a garbage string returns None instead of raising. parse throws NumberParseException on input it can't even tokenize:

import phonenumbers
from phonenumbers import PhoneNumberFormat

def to_e164(raw, region=None):
    try:
        number = phonenumbers.parse(raw, region)
    except phonenumbers.NumberParseException:
        return None
    if not phonenumbers.is_valid_number(number):
        return None
    return phonenumbers.format_number(number, PhoneNumberFormat.E164)

Store the E.164 result, not the string the user typed. (For why that one format outranks the others, see what E.164 is and why it wins.) If you also do this in a Node service, the JavaScript walkthrough is validating phone numbers in JavaScript — same library, same rules, so your two stacks agree.

number_type is metadata — and it can be stale

phonenumbers also classifies a number:

from phonenumbers import PhoneNumberType

phonenumbers.number_type(number)   # PhoneNumberType.FIXED_LINE_OR_MOBILE

Read that result honestly. For US numbers you will very often get FIXED_LINE_OR_MOBILE — the metadata can't separate the two, so the honest reading is "I don't know which." That's a feature: the library declines to guess.

It also ships two submodules that look like they answer the next question, and this is where people get burned:

from phonenumbers import carrier, geocoder

number = phonenumbers.parse("+16504472983", None)

carrier.name_for_number(number, "en")          # the carrier the block was ASSIGNED to
geocoder.description_for_number(number, "en")  # "San Francisco, CA" — a label, not a location

Both read from static, bundled metadata. carrier.name_for_number returns the carrier a number range was originally allocated to — which number portability makes wrong the moment a subscriber switches networks. geocoder returns the region a number was issued in, not where the handset is. Neither is a live lookup, and for portable US numbers carrier frequently returns an empty string because it simply doesn't know. Feed a "carrier" derived from this into a routing or fraud decision and you're acting on a guess that ages every day.

Where an API fits — and where it doesn't

Here's the honest boundary. Everything above runs offline, for free, and for validating and normalizing a number it is genuinely all you need. You do not need an API to call is_valid_number.

A validation API earns its place at two specific edges. First, one consistent contract across languages: your Python worker, your Node service, and your no-code automation all hit the same endpoint and get the same answer, instead of each pinning its own libphonenumber build. Second — the part offline data cannot do — the real carrier and liveness layer, available on request with hlr:true: the current carrier for a ported number, and HLR liveness that checks whether a line is actually reachable right now. Boundstone doesn't do those today (more on that below).

import requests

resp = requests.post(
    "https://api.boundstone.io/v1/verify/phone",
    headers={"Authorization": "Bearer bs_live_YOUR_KEY"},
    json={"phone": "+16504472983"},
)
data = resp.json()
data["valid"]  # True
data["e164"]   # "+16504472983"

The honesty contract

Boundstone's answer for a phone number tells you exactly what it did and did not check:

{
  "checks": {
    "performed": ["format", "region", "line_type_metadata"],
    "not_performed": ["carrier_lookup", "ported_status", "hlr_liveness"]
  }
}

Today, carrier_lookup, ported_status, and hlr_liveness sit in not_performed — and that's deliberate. We'd rather report "not checked" than hand you the static carrier guess that phonenumbers.carrier would give, because a stale carrier looks like an answer and isn't. Those live checks are a paid opt-in: send hlr:true (5 credits, paid plans) and they move into performed only when Boundstone actually runs them, refunded when the network cannot answer. Until then, a "valid": true from us means format and region — the same thing the offline library can tell you, plus one contract your whole stack shares.

The short version

  • Validating a phone number in Python? Use phonenumbers: parse, is_valid_number, format_number(..., PhoneNumberFormat.E164). Offline, free, and enough.
  • number_type, carrier and geocoder are static metadata. Treat carrier/geocoder as origin labels, never as a live carrier or location.
  • Reach for the API for one contract across languages — or for the live carrier/HLR layer, which Boundstone runs on request with hlr:true rather than faking.
  • Kick the tires without a key at the free phone validator.

Frequently asked questions

How do I validate a phone number in Python?

The standard approach is Google's libphonenumber through the Python "phonenumbers" package, which parses a number and checks its format, region, and line-type metadata. If you would rather not manage that library and its metadata updates yourself, a single API call returns the same checks: Boundstone's phone endpoint performs format, region, and line-type-metadata validation and returns the E.164 form, country, and line type. Both approaches confirm a number is well-formed and dialable in its region, which is enough to clean a list before you use it.

What does a "valid" phone number result actually prove?

A "valid" result means the number is correctly formatted and falls within a real dialing range for its region, along with a line-type guess (such as mobile or fixed line) drawn from static metadata. It does not prove the number is currently active, reachable, or that it has not been ported to a different carrier. Boundstone is explicit about this: every response lists carrier_lookup, ported_status, and hlr_liveness under checks.not_performed, so you always know exactly what a "valid" did and did not confirm.

Can Python tell me if a phone number is active or in service?

No. Format and metadata validation, whether in the phonenumbers library or via an API, cannot tell you whether a line is live, because that requires a carrier or HLR network query rather than a syntax check. Boundstone reports carrier_lookup and hlr_liveness as not_performed by default, though a live HLR dip is available as a paid opt-in (hlr:true, 5 credits). For now, treat "valid" as "correctly formed and dialable," and do not assume it means someone will answer.

Thomas Tsui

Founder of Boundstone — building phone, email and IP validation you can actually verify.

One honest API for email, phone and IP — every response lists what it checked and what it didn't claim to. Free tier: 250 credits/month, no card, credits never expire.

More from Boundstone — API documentation · Benchmark methodology · The benchmark series · Buyer's checklist