Skip to content

Phone

The phone validator parses, normalizes, and decodes Sri Lankan phone numbers. Given any input string, it tells you whether the number is valid, what the canonical form is, who the carrier is, and whether it's a mobile or fixed-line number.

Quick start

from helakit import validate_phone, is_valid_phone

result = validate_phone("0712345678")

result.is_valid       # True
result.normalized     # "+94712345678"
result.carrier        # "Mobitel"
result.line_type      # "mobile"
result.local          # "0712345678"

is_valid_phone("0712345678")  # True

validate_phone returns a PhoneResult, a ValidationResult subclass with typed properties for every field the validator extracts.

What "valid" means

A Sri Lankan phone number is considered valid when all of the following hold:

  1. The string contains only ASCII digits, optionally preceded by a +. Spaces, hyphens, and parentheses are tolerated and stripped.
  2. The number can be coerced into a 10-digit local form (0XXXXXXXXX) — either it already is one, or it's in international form (+94XXXXXXXXX or 94XXXXXXXXX) and can be converted.
  3. The leading 3 digits of the local form are a recognised Sri Lankan mobile or fixed-line prefix (the full table is in Recognised prefixes below).

If any check fails, the result is invalid and errors contains one ValidationError explaining which rule was broken. The validator short-circuits on the first hard failure, so you will see at most one error per call today.

Accepted input forms

The validator is permissive about whitespace and punctuation: spaces, hyphens, and parentheses are stripped before validation. It is strict about everything else.

validate_phone("0712345678").is_valid       # True
validate_phone("071 234 5678").is_valid     # True
validate_phone("071-234-5678").is_valid     # True
validate_phone("(071) 234-5678").is_valid   # True
validate_phone("+94712345678").is_valid     # True
validate_phone("+94 71 234 5678").is_valid  # True
validate_phone("94712345678").is_valid      # True
validate_phone("712345678")     # missing leading 0 / country code
validate_phone("07ABCDEFGH")    # non-digit characters
validate_phone("٠٧١٢٣٤٥٦٧٨")    # Unicode (Arabic-Indic) digits
validate_phone("07+12345678")   # '+' anywhere but the start

Unicode digits are rejected on purpose

Although Python's str.isdigit() returns True for Arabic-Indic and other Unicode digits, those characters mean different things in different locales. The validator only accepts [0-9] to avoid surprising round-trips through int() and database storage.

Reading the result

A PhoneResult supports four equivalent access patterns. They all read the same underlying data — pick whichever fits the surrounding code.

result = validate_phone("0712345678")

# 1. Typed attribute access (preferred — autocomplete + type checks)
result.carrier            # "Mobitel"
result.line_type          # "mobile"
result.local              # "0712345678"
result.decoded            # PhoneDecoded(carrier='Mobitel', ...)

# 2. Dict-style — useful if the field name is dynamic
result["carrier"]         # "Mobitel"

# 3. Safe access with default
result.get("carrier", "Unknown")

# 4. The underlying dict, for power users
result.data["carrier"]

result.carrier returns None on an invalid result; result["carrier"] raises KeyError. See Working with results for the full comparison table.

Available fields

Property Type Description
is_valid bool Whether the number passed every check.
value str The original input, unmodified.
normalized str \| None Canonical "+94XXXXXXXXX" form. None if invalid.
carrier str \| None Network operator name (e.g. "Mobitel").
line_type "mobile" \| "fixed" \| None Line classification.
local str \| None 10-digit local form ("0XXXXXXXXX").
decoded PhoneDecoded \| None All three fields above bundled.
errors list[ValidationError] Empty when valid.
data dict[str, Any] Raw payload behind the typed properties.

Error codes

The phone validator can return one of four error codes. They are stable across releases — match on err.code, not on err.message.

phone.invalid_characters

The input contains a character other than ASCII digits or a leading +. Triggered by letters, internal +, Unicode digits, special characters, and empty strings.

result = validate_phone("07ABCDEFGH")
result.errors[0].code     # "phone.invalid_characters"
result.errors[0].message  # "Phone number must contain digits only ..."

phone.missing_prefix

The input is otherwise clean but does not start with 0 (local), +94 (international with +), or 94 (international without +).

result = validate_phone("712345678")
result.errors[0].code  # "phone.missing_prefix"

phone.invalid_length

The input has the right shape but the wrong number of digits. The local form must be exactly 10 digits.

result = validate_phone("071234567")    # 9 digits
result.errors[0].code  # "phone.invalid_length"

result = validate_phone("07123456789")  # 11 digits
result.errors[0].code  # "phone.invalid_length"

phone.unknown_prefix

The three-digit local prefix (e.g. "071", "011") is not a recognised Sri Lankan mobile or fixed-line prefix. This is the most useful error in practice — it catches well-formed numbers that simply aren't valid SL numbers.

result = validate_phone("0001234567")
result.errors[0].code  # "phone.unknown_prefix"

Reacting to errors

A match statement on the error code is the idiomatic way to branch on why a number failed.

result = validate_phone(user_input)
if not result:
    match result.errors[0].code:
        case "phone.invalid_characters":
            ask_user_to_re_enter()
        case "phone.unknown_prefix":
            ask_if_foreign_number()
        case "phone.invalid_length" | "phone.missing_prefix":
            show_format_hint()

For the complete list of error codes used across all validators, see Error handling.

Exceptions

validate_phone and is_valid_phone raise — rather than return an invalid result — for programmer errors:

Exception When
helakit.InvalidInputError value is not a string (e.g. None, int).

Bad data (a malformed phone number) is not an exception — it comes back as a normal invalid result. Reserve try/except for the programmer-error case.

from helakit import InvalidInputError, validate_phone

try:
    validate_phone(None)
except InvalidInputError as e:
    ...  # caller passed the wrong type

Recognised prefixes

The validator recognises the following three-digit local prefixes. The data lives in helakit.phone._data and is sourced from the Telecommunications Regulatory Commission of Sri Lanka (TRCSL).

Prefix Carrier
070, 076, 077 Dialog
074 Dialog (special)
071, 072 Mobitel
075 Airtel
078 Hutch
079 Lanka Bell
Prefix City / Region
011 Colombo (SLT / Dialog)
031 Negombo
032 Kurunegala
033 Gampaha
034 Kalutara
035 Kegalle
036 Avissawella
037 Kurunegala (alt)
038 Panadura
041 Galle
045 Ratnapura
047 Hambantota
051, 052 Nuwara Eliya / Kandy
054 Matale
055 Badulla
057 Bandarawela
063 Ampara
065 Batticaloa
066 Polonnaruwa
067 Kalmunai
081 Kandy
091 Galle (alt)

Found a missing prefix?

Prefix lists change occasionally as new operators are licensed. Please open an issue or PR if you spot one that's missing or misclassified.

Recipes

Validate a column of numbers (pandas)

import pandas as pd
from helakit import validate_phone

df = pd.DataFrame({"phone": ["0712345678", "94772345678", "bogus"]})

results = df["phone"].map(validate_phone)
df["is_valid"]   = results.map(bool)
df["normalized"] = results.map(lambda r: r.normalized)
df["carrier"]    = results.map(lambda r: r.carrier)
df["line_type"]  = results.map(lambda r: r.line_type)

Filter to mobile numbers only

mobile_numbers = [n for n in numbers if (r := validate_phone(n)) and r.line_type == "mobile"]

Normalize for storage

def normalize(n: str) -> str | None:
    """Return the canonical +94 form, or None if not a valid SL number."""
    result = validate_phone(n)
    return result.normalized if result else None

PhoneResult

Bases: ValidationResult

Validation result returned by :func:~helakit.phone.validate_phone.

Adds typed property accessors for every field the phone validator extracts. The underlying data dict is still populated for backwards compatibility, so every access style works::

result.carrier        # typed property — preferred
result["carrier"]     # dict-style access
result.data["carrier"]  # original form

Properties return None on invalid results so attribute access never raises — guard with if result: or if result.is_valid:.

PhoneDecoded

Structured metadata about a recognised Sri Lankan phone number.

Returned in PhoneResult.data["decoded"] and also accessible as PhoneResult.decoded. Bundles the three pieces of information that can be derived from a number's prefix.

Attributes:

Name Type Description
carrier str

Network operator name (e.g. "Dialog", "Mobitel", "SLT"). Determined by the three-digit local prefix.

line_type LineType

Either "mobile" or "fixed".

local str

The 10-digit local form ("0XXXXXXXXX"). The international form is on the result as normalized.

Example

result = validate_phone("+94712345678") result.decoded PhoneDecoded(carrier='Mobitel', line_type='mobile', local='0712345678')