mrkeyoor.com_
Fri 07 Aug 20:53 UTC
PyPIUtilsupdated 07 Aug 2026

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.

Verdict

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.

API stability5/5Function names track upstream libphonenumber, so the surface only moves when Google's does, and it has barely moved in years. The 9.0.x line is at release 36 and those releases carry metadata refreshes rather than signature changes, which makes upgrades close to risk-free.
Docs3/5The README walks the common tasks with a real REPL transcript covering parsing, formatting, as-you-type, matching, geocoding, carrier and timezone, and there is a generated API site. Beyond that it points you at the unit tests or the Java project, so matcher leniency levels, short number costs and the possible-versus-valid distinction have no prose guide.
Maintenance5/5One maintainer has tracked upstream libphonenumber since 2011 and is still doing it: 9.0.36 shipped 1 August 2026 with the repository pushed the same day, and the tracker sits at 10 open issues plus 1 open PR. For a metadata-driven library, release cadence is the maintenance story, and this one releases every few weeks.
Ecosystem4/5Around 9.04M weekly downloads and the default choice underneath django-phonenumber-field and pydantic-extra-types, with phonenumberslite covering size-constrained deployments. It is a port rather than a platform, so there is no plugin surface; you either use its functions or you do not.

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
Skip it if

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_OK

In 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 changes

The 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_RATE

Regular 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_MATCH

It 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

PackageRegistryPick it when
phonenumberslitePyPIYou need the same parsing and formatting API but cannot afford the geocoder, carrier and timezone metadata
django-phonenumber-fieldPyPIYou are on Django and want a model field, form field and serializer that wrap this library for you
pydantic-extra-typesPyPIYou validate request bodies with pydantic and want a PhoneNumber type that parses and normalises during model validation