phonenumbers
phonenumbers is a Python port of Google's libphonenumber, the same library that formats numbers in Android's dialer, including a translation of Google's numbering plan metadata for every country. The core object is PhoneNumber, which you get by calling parse() on a string plus the region the number is being dialled from. Once you have one, you can ask whether it is possible (right shape and length) or valid (matches an assigned range), format it as E.164, national, international or RFC3966, and find out whether it is a mobile, a fixed line, a toll-free number or a VOIP number. Around that core sit the extras: an AsYouTypeFormatter for input fields, a PhoneNumberMatcher that pulls numbers out of free text, a short-number module for emergency and service codes, and separate geocoder, carrier and timezone packages that map a number to a place, an original network operator and a set of IANA time zones. Everything runs locally with no network calls, which is also why the package carries megabytes of generated metadata.
If you handle phone numbers at all, this is the library, because the value is Google's metadata rather than the code and nobody else maintains a Python copy of it. Just keep upgrading it, and never let anyone on the team read is_valid_number as proof that someone will answer.
Use it if
- You store user phone numbers and want one canonical form in the database, which means parse to a PhoneNumber then format to E.164 before it ever reaches your schema
- You need to reject nonsense at input time in a way a regex cannot manage, because valid length and prefix rules differ per country and change over time
- You want live formatting as the user types, which AsYouTypeFormatter does digit by digit for the region you give it
- You need to know a number's type before acting on it, for example refusing to queue an SMS to a fixed line or a premium rate number
- You have to extract numbers from unstructured text such as emails, scraped listings or support tickets, which is what PhoneNumberMatcher is for
- You want a rough location, original carrier or time zone for a number without calling an external API
- You are treating validity as reachability. is_valid_number only says the number matches an assigned range in Google's metadata; it says nothing about whether the line exists, is switched on, or can receive SMS. If you need that answer, you need an HLR lookup service and this library is not it.
- Your deployment is size-constrained. The core metadata is over 2 MiB of generated Python loaded region by region, the geocoding metadata is around 19 MiB, carrier around 1 MiB and timezone around 100 KiB. On a small serverless function that is a real part of your budget, and phonenumberslite exists specifically because of it.
- You plan to install it once and forget it. Numbering plans change, so the metadata goes stale; the 9.0.x line is already at release 36, with 9.0.36 published on 1 August 2026. Pinning for a year means answering validity questions with last year's rules.
- You need predictable first-call latency. Region metadata is imported lazily on the first number from that region, so the first parse for a new country pays an import cost inside your request unless you force-load at startup.
- The field is low stakes and you only want to know whether a string vaguely looks like a phone number. Pulling in this much metadata for a soft check on a marketing form is not a good trade; the lite package or a length check is cheaper.
- You expect an interface designed for Python. This is a faithful port of a Java library, so you get module-level functions taking a number object rather than methods, snake_case renames of Java names, and a README that tells you to read the unit tests or the upstream project for anything it does not cover.
Setup reality
pip install phonenumbers is pure Python with no compiled extensions and no dependencies, and requires_python is declared as 2.5 or newer, so it installs on essentially anything. What you are really installing is data: the wheel is large because Google's metadata ships as generated Python modules. phonenumberslite is the same API minus the geocoder, carrier and timezone subpackages, and importing phonenumbers.geocoder against the lite package fails, so pick one per environment rather than mixing them. Type stubs for Python 3 are bundled, so mypy and ty see the signatures without a separate stubs package. The behaviour that catches everyone first is parse(): anything not already in +E.164 form needs a default region, and passing None raises NumberParseException instead of guessing, which means you have to know where your user is before you can validate their number. The second is that parse() throws away the original string unless you pass keep_raw_input=True, so if you later want to show what the user typed or inspect country_code_source you have to ask for it up front. In a long-running service, call PhoneMetadata.load_all() and import the geocoder, carrier and timezone packages at start so the memory shows up in your baseline rather than mid-request.
Patterns
Parse a number and check itparse-and-validate
import phonenumbers
num = phonenumbers.parse("020 8366 1177", "GB")
phonenumbers.is_possible_number(num) # right length for GB
phonenumbers.is_valid_number(num) # in an assigned range
# already international, no region needed
phonenumbers.parse("+442083661177", None)is_possible_number is a cheap length and prefix check; is_valid_number does the full range match and is the one you want before storing. Neither tells you the line is reachable. The second argument is the region the number is dialled from, not the region it belongs to.
Normalise to E.164 before writing to the databaseformat-for-storage
from phonenumbers import PhoneNumberFormat, format_number, parse
num = parse("(650) 253-2222", "US")
format_number(num, PhoneNumberFormat.E164) # '+16502532222'
format_number(num, PhoneNumberFormat.INTERNATIONAL) # '+1 650-253-2222'
format_number(num, PhoneNumberFormat.NATIONAL) # '(650) 253-2222'
format_number(num, PhoneNumberFormat.RFC3966) # 'tel:+1-650-253-2222'Store E164 and nothing else, then format for display at read time. E164 strips all punctuation and extensions, so if you need an extension keep it in its own column; the RFC3966 form is the one that preserves it as ;ext=.
Deal with input that is not a phone numberhandle-parse-errors
import phonenumbers
from phonenumbers import NumberParseException
def normalise(raw: str, region: str | None) -> str | None:
try:
num = phonenumbers.parse(raw, region)
except NumberParseException:
return None
if not phonenumbers.is_valid_number(num):
return None
return phonenumbers.format_number(
num, phonenumbers.PhoneNumberFormat.E164)parse raises rather than returning None, and it raises for two very different reasons: the string is not number-shaped at all, or it is a national number with no region supplied. Both surface as NumberParseException, so check exc.error_type if you want to tell the user which mistake they made.
Find out if you can text itnumber-type
from phonenumbers import PhoneNumberType, number_type, parse
kind = number_type(parse("+447986123456", None))
kind == PhoneNumberType.MOBILE
SMS_OK = {PhoneNumberType.MOBILE, PhoneNumberType.FIXED_LINE_OR_MOBILE}
can_sms = number_type(num) in SMS_OKIn the US and Canada the metadata cannot separate mobiles from fixed lines, so you get FIXED_LINE_OR_MOBILE and have to decide what to do with the ambiguity. Treating that value as not-mobile will block most American users.
Format an input field while the user typesas-you-type-formatting
from phonenumbers import AsYouTypeFormatter
fmt = AsYouTypeFormatter("US")
for digit in "6502532222":
display = fmt.input_digit(digit)
# '(650) 253-2222'
fmt.clear() # reset when the field is cleared or the region changesThe formatter is stateful and only moves forward, so a backspace means calling clear() and replaying the remaining digits rather than removing one. Changing the region needs a new instance; there is no way to retarget an existing one.
Extract numbers from free textfind-numbers-in-text
from phonenumbers import PhoneNumberMatcher, PhoneNumberFormat, format_number
text = "Call 510-748-8230 before 9:30, or 703-4800500 after 10am."
for match in PhoneNumberMatcher(text, "US"):
print(match.start, match.end, match.raw_string,
format_number(match.number, PhoneNumberFormat.E164))Every match carries its offsets and the raw substring, which is what you need to redact or link numbers in place. Default leniency is deliberately conservative, so numbers written with unusual separators are skipped rather than mangled.
Loosen or tighten what counts as a matchmatcher-leniency
from phonenumbers import Leniency, PhoneNumberMatcher
strict = PhoneNumberMatcher(text, "US", leniency=Leniency.VALID)
loose = PhoneNumberMatcher(text, "US", leniency=Leniency.POSSIBLE)
exact = PhoneNumberMatcher(text, "US", leniency=Leniency.EXACT_GROUPING,
max_tries=100)POSSIBLE will happily match order numbers, invoice ids and dates, so only use it when a human reviews the output. max_tries caps how many candidates are examined, which is worth setting when you run the matcher over documents you did not write.
Describe where a number is fromgeocode-carrier-timezone
import phonenumbers
from phonenumbers import carrier, geocoder, timezone
ch = phonenumbers.parse("0431234567", "CH")
geocoder.description_for_number(ch, "en") # 'Zurich'
ro = phonenumbers.parse("+40721234567", None)
carrier.name_for_number(ro, "en") # 'Vodafone'
timezone.time_zones_for_number(
phonenumbers.parse("+447986123456", None))Carrier data reflects the operator a range was originally allocated to, so after number portability it is frequently wrong; treat it as a hint, never as billing input. These three subpackages are missing from phonenumberslite, and importing them there raises ImportError.
Pay the metadata cost at startup instead of in a requestpreload-metadata
from phonenumbers import PhoneMetadata
import phonenumbers.geocoder # noqa: F401
import phonenumbers.carrier # noqa: F401
import phonenumbers.timezone # noqa: F401
PhoneMetadata.load_all()Core metadata loads lazily per region, so without this the first Brazilian number in production pays an import inside the request and your p99 shows it. Doing this also means the memory appears in your container baseline, which is what you want when sizing limits.
Recognise emergency and service codesshort-numbers
from phonenumbers import ShortNumberCost, parse, shortnumberinfo
num = parse("112", "GB")
shortnumberinfo.is_valid_short_number(num)
shortnumberinfo.connects_to_emergency_number("999", "GB")
shortnumberinfo.expected_cost(parse("118118", "GB")) == ShortNumberCost.PREMIUM_RATERegular is_valid_number returns False for short codes, because they are not in the normal numbering plan; shortnumberinfo is a separate metadata set with its own functions. Check expected_cost before dialling anything programmatically, since premium rate short codes live in the same space as free service numbers.
Generate real test fixtures per countryexample-numbers-for-tests
from phonenumbers import (PhoneNumberType, example_number,
example_number_for_type, invalid_example_number)
example_number("DE")
example_number_for_type("IN", PhoneNumberType.MOBILE)
invalid_example_number("BR")These come from the same metadata as validation, so a fixture built this way stays correct when the numbering plan changes and the library is upgraded. invalid_example_number is the honest way to test your rejection path instead of inventing a string that might become valid later.
Decide whether two strings are the same numbercompare-two-numbers
import phonenumbers
from phonenumbers import MatchType, is_number_match
is_number_match("+44 20 8366 1177", "020 8366 1177")
# MatchType.NSN_MATCH: national numbers agree, country code unproven
is_number_match("+442083661177", "+442083661177")
# MatchType.EXACT_MATCHIt returns a MatchType rather than a boolean, and NSN_MATCH versus EXACT_MATCH is the distinction that matters for deduplication: the former means one side had no country code, so two subscribers in different countries can produce it.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| phonenumberslite | PyPI | You need the same parsing and formatting API but cannot afford the geocoder, carrier and timezone metadata |
| django-phonenumber-field | PyPI | You are on Django and want a model field, form field and serializer that wrap this library for you |
| pydantic-extra-types | PyPI | You validate request bodies with pydantic and want a PhoneNumber type that parses and normalises during model validation |