phonenumbers parses, validates, and formats phone numbers using Google’s libphonenumber. pip install phonenumbers. Parse: import phonenumbers; num = phonenumbers.parse("+14155552671"). phonenumbers.parse("(415) 555-2671", "US") — with region. Validate: phonenumbers.is_valid_number(num) → True/False. phonenumbers.is_possible_number(num) — quick check (less strict). Format: phonenumbers.format_number(num, phonenumbers.PhoneNumberFormat.E164) → “+14155552671”. PhoneNumberFormat.INTERNATIONAL → “+1 415-555-2671”. PhoneNumberFormat.NATIONAL → “(415) 555-2671”. PhoneNumberFormat.RFC3966 → “tel:+1-415-555-2671”. Country code: num.country_code → 1. num.national_number. Region: phonenumbers.region_code_for_number(num) → “US”. Geo: from phonenumbers import geocoder; geocoder.description_for_number(num, "en") → “California”. Carrier: from phonenumbers import carrier; carrier.name_for_number(num, "en"). Timezone: from phonenumbers import timezone; timezone.time_zones_for_number(num) → frozenset. Type: phonenumbers.number_type(num) — MOBILE FIXED_LINE TOLL_FREE etc. PhoneNumberType. from phonenumbers import number_type, PhoneNumberType. number_type(num) == PhoneNumberType.MOBILE. Example: phonenumbers.example_number_for_type("US", PhoneNumberType.MOBILE). Error: phonenumbers.NumberParseException. Normalize: phonenumbers.normalize_digits_only("(415) 555-2671") → “4155552671”. Claude Code generates phonenumbers validators, formatters, and bulk parsing pipelines.
CLAUDE.md for phonenumbers
## phonenumbers Stack
- Version: phonenumbers >= 8.13 | pip install phonenumbers
- Parse: phonenumbers.parse("+14155552671") | parse("(415) 555-2671", "US") with region
- Validate: is_valid_number(num) | is_possible_number(num) for quick check
- Format: format_number(num, PhoneNumberFormat.E164) | INTERNATIONAL | NATIONAL
- Region: region_code_for_number(num) → "US" | num.country_code → 1
- Geo: geocoder.description_for_number(num, "en") → "California"
- Type: number_type(num) == PhoneNumberType.MOBILE | FIXED_LINE | TOLL_FREE
phonenumbers Phone Number Pipeline
# app/phone_utils.py — phonenumbers parsing, validation, formatting, and enrichment
from __future__ import annotations
from typing import Any
import phonenumbers
from phonenumbers import (
NumberParseException,
PhoneNumberFormat,
PhoneNumberType,
carrier,
geocoder,
timezone,
)
from phonenumbers import (
format_number as _format,
is_possible_number,
is_valid_number,
number_type,
parse as _parse,
region_code_for_number,
)
# ─────────────────────────────────────────────────────────────────────────────
# 1. Parse helpers
# ─────────────────────────────────────────────────────────────────────────────
def parse_phone(
text: str,
default_region: str = "US",
) -> phonenumbers.PhoneNumber | None:
"""
Parse a phone number string.
default_region: ISO 3166-1 alpha-2 code used when the number has no country prefix.
Returns None on parse failure (invalid format, unsupported region).
"""
try:
return _parse(text, default_region)
except NumberParseException:
return None
def parse_e164(e164: str) -> phonenumbers.PhoneNumber | None:
"""
Parse an E.164 number (e.g. "+14155552671").
No region hint needed — E.164 always starts with country code.
"""
try:
return _parse(e164, None)
except NumberParseException:
return None
# ─────────────────────────────────────────────────────────────────────────────
# 2. Validation
# ─────────────────────────────────────────────────────────────────────────────
def validate(text: str, region: str = "US") -> bool:
"""
Full number validity check: correct length, valid area code, etc.
is_valid_number() uses the complete libphonenumber metadata.
Returns False for impossible numbers (e.g. US number with wrong digit count).
"""
num = parse_phone(text, region)
if num is None:
return False
return is_valid_number(num)
def is_possible(text: str, region: str = "US") -> bool:
"""
Quick check: correct length for the country/region, no deep metadata lookup.
Faster than is_valid_number, but less accurate — may accept some invalid numbers.
"""
num = parse_phone(text, region)
if num is None:
return False
return is_possible_number(num)
def validate_batch(
numbers: list[str],
region: str = "US",
) -> list[dict[str, Any]]:
"""
Validate a list of phone numbers.
Returns [{"input", "valid", "possible", "e164"?}].
"""
results = []
for text in numbers:
num = parse_phone(text, region)
valid = is_valid_number(num) if num else False
possible = is_possible_number(num) if num else False
e164 = _format(num, PhoneNumberFormat.E164) if valid else None
results.append({
"input": text,
"valid": valid,
"possible": possible,
"e164": e164,
})
return results
# ─────────────────────────────────────────────────────────────────────────────
# 3. Formatting
# ─────────────────────────────────────────────────────────────────────────────
def to_e164(text: str, region: str = "US") -> str | None:
"""
Convert any phone string to E.164 format (+14155552671).
E.164 is the standard for databases, APIs, and SMS gateways.
Returns None if the number is invalid.
"""
num = parse_phone(text, region)
if num is None or not is_valid_number(num):
return None
return _format(num, PhoneNumberFormat.E164)
def to_international(text: str, region: str = "US") -> str | None:
"""Format as international: '+1 415-555-2671'."""
num = parse_phone(text, region)
if num is None or not is_valid_number(num):
return None
return _format(num, PhoneNumberFormat.INTERNATIONAL)
def to_national(text: str, region: str = "US") -> str | None:
"""Format as national (local): '(415) 555-2671'."""
num = parse_phone(text, region)
if num is None or not is_valid_number(num):
return None
return _format(num, PhoneNumberFormat.NATIONAL)
def normalize_phone(text: str, region: str = "US") -> str | None:
"""
Normalize any phone format to E.164 — the canonical storage format.
Strips parentheses, dashes, spaces, and adds country code.
"""
return to_e164(text, region)
def format_for_display(text: str, region: str = "US") -> str | None:
"""
Format a phone number for user-facing display.
Uses INTERNATIONAL format for non-local numbers, NATIONAL for local.
"""
num = parse_phone(text, region)
if num is None or not is_valid_number(num):
return None
detected_region = region_code_for_number(num)
if detected_region == region:
return _format(num, PhoneNumberFormat.NATIONAL)
return _format(num, PhoneNumberFormat.INTERNATIONAL)
# ─────────────────────────────────────────────────────────────────────────────
# 4. Enrichment — geo, carrier, timezone, type
# ─────────────────────────────────────────────────────────────────────────────
_TYPE_NAMES = {
PhoneNumberType.FIXED_LINE: "fixed_line",
PhoneNumberType.MOBILE: "mobile",
PhoneNumberType.FIXED_LINE_OR_MOBILE: "fixed_or_mobile",
PhoneNumberType.TOLL_FREE: "toll_free",
PhoneNumberType.PREMIUM_RATE: "premium_rate",
PhoneNumberType.SHARED_COST: "shared_cost",
PhoneNumberType.VOIP: "voip",
PhoneNumberType.PERSONAL_NUMBER: "personal",
PhoneNumberType.PAGER: "pager",
PhoneNumberType.UAN: "uan",
PhoneNumberType.VOICEMAIL: "voicemail",
PhoneNumberType.UNKNOWN: "unknown",
}
def enrich(
text: str,
region: str = "US",
lang: str = "en",
) -> dict[str, Any]:
"""
Parse a phone number and return all available metadata.
Raises ValueError for invalid numbers.
"""
num = parse_phone(text, region)
if num is None:
raise ValueError(f"Cannot parse phone number: {text!r}")
valid = is_valid_number(num)
ntype = number_type(num)
result: dict[str, Any] = {
"input": text,
"valid": valid,
"country_code": num.country_code,
"national": num.national_number,
"region": region_code_for_number(num),
"type": _TYPE_NAMES.get(ntype, "unknown"),
"e164": _format(num, PhoneNumberFormat.E164) if valid else None,
"international": _format(num, PhoneNumberFormat.INTERNATIONAL) if valid else None,
"national_fmt": _format(num, PhoneNumberFormat.NATIONAL) if valid else None,
}
if valid:
try:
result["location"] = geocoder.description_for_number(num, lang) or None
except Exception:
result["location"] = None
try:
result["carrier"] = carrier.name_for_number(num, lang) or None
except Exception:
result["carrier"] = None
try:
result["timezones"] = list(timezone.time_zones_for_number(num))
except Exception:
result["timezones"] = []
return result
def get_timezones(text: str, region: str = "US") -> list[str]:
"""Return the timezone(s) associated with a phone number."""
num = parse_phone(text, region)
if num is None or not is_valid_number(num):
return []
return list(timezone.time_zones_for_number(num))
# ─────────────────────────────────────────────────────────────────────────────
# 5. Pandas integration
# ─────────────────────────────────────────────────────────────────────────────
def normalize_phone_series(values: list[str | None], region: str = "US") -> list[str | None]:
"""
Normalize a list of phone strings to E.164.
Invalid/unparseable numbers → None.
For pandas: df["phone_e164"] = normalize_phone_series(df["phone"].tolist())
"""
return [normalize_phone(str(v), region) if v else None for v in values]
def validate_phone_series(values: list[str | None], region: str = "US") -> list[bool]:
"""Validate a list — returns list of True/False."""
return [validate(str(v), region) if v else False for v in values]
# ─────────────────────────────────────────────────────────────────────────────
# 6. Jinja2 filter registration
# ─────────────────────────────────────────────────────────────────────────────
def register_phone_filters(env, default_region: str = "US") -> None:
"""
Register phone formatting filters for Jinja2.
Usage:
{{ user.phone | phone_e164 }}
{{ user.phone | phone_intl }}
{{ user.phone | phone_national }}
"""
env.filters["phone_e164"] = lambda t: to_e164(t, default_region)
env.filters["phone_intl"] = lambda t: to_international(t, default_region)
env.filters["phone_national"] = lambda t: to_national(t, default_region)
env.filters["phone_display"] = lambda t: format_for_display(t, default_region)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
test_numbers = [
("+14155552671", None),
("415-555-2671", "US"),
("(800) 555-1234","US"),
("+44 20 7123 1234", None),
("+81 3-1234-5678", None),
("+49 30 12345678", None),
("+33 1 23 45 67 89",None),
("not-a-number", "US"),
("", "US"),
]
print("=== Validation and formats ===")
for text, region in test_numbers:
reg = region or "auto"
num = parse_phone(text, region or "US")
valid = is_valid_number(num) if num else False
e164 = _format(num, PhoneNumberFormat.E164) if valid else "—"
intl = _format(num, PhoneNumberFormat.INTERNATIONAL) if valid else "—"
print(f" {str(text):25} [{reg:4}] valid={valid} e164={e164:18} intl={intl}")
print("\n=== Batch validation ===")
batch = validate_batch(["415-555-2671", "invalid", "+44 20 7123 1234"])
for r in batch:
print(f" {r['input']:20} valid={r['valid']} e164={r['e164']}")
print("\n=== Enrichment (+14155552671) ===")
info = enrich("+14155552671")
for k, v in info.items():
if v is not None:
print(f" {k:15}: {v}")
print("\n=== Enrichment (UK number) ===")
uk = enrich("+44 20 7123 1234")
for k, v in uk.items():
if v is not None:
print(f" {k:15}: {v}")
For the re (regex) alternative — a regex like r"\+?1?[\s\-]?\(?\d{3}\)?[\s\-]\d{3}[\s\-]\d{4}" can match common US number patterns but fails on international formats, doesn’t validate area codes, and can’t detect whether a number is a mobile, toll-free, or VoIP line; phonenumbers uses Google’s libphonenumber metadata — the same data Google uses — so is_valid_number() knows that US numbers start with a valid area code and that +44 20 is a London geographic number. For the pydantic PhoneNumber field alternative — pydantic-extra-types includes a PhoneNumber type that validates on model parse and stores E.164, but it uses phonenumbers internally — reaching for phonenumbers directly gives you the full enrichment API (geocoder, carrier, timezone) that pydantic’s wrapper doesn’t expose. The Claude Skills 360 bundle includes phonenumbers skill sets covering phonenumbers.parse() with region hint, is_valid_number() and is_possible_number(), PhoneNumberFormat.E164/INTERNATIONAL/NATIONAL/RFC3966, region_code_for_number(), geocoder.description_for_number(), carrier.name_for_number(), timezone.time_zones_for_number(), number_type() with PhoneNumberType constants, enrich() comprehensive metadata dict, validate_batch() list validator, normalize_phone_series() pandas integration, and Jinja2 phone filter registration. Start with the free tier to try phone number parsing and validation code generation.