Skip to main content
Regex Guide

How to Match Phone Numbers
with Regex

Patterns that actually work — E.164, US, UK, and international formats, with common pitfalls and normalisation one-liners.

8 min read·Updated May 2026

How to match phone numbers with regex: no single pattern covers every country, so the right approach is to pick the strictest pattern your use case allows — E.164 for storage and APIs, a locale-specific pattern for form validation, and a loose permissive pattern only when extracting from unstructured text.

Why Phone Number Regex Is Hard

Phone numbers are deceptively tricky to match with regex because there is no universal format. The same physical number — say, a London mobile — can be written at least five different ways, all of them valid:

# Five valid representations of the same UK mobile number
+44 7911 123456       # E.164-ish with spaces
+447911123456         # Strict E.164 (no spaces)
07911 123456          # UK local format (leading 0, no country code)
07911-123-456         # Dashes as separators
+44 (0)7911 123456   # Country code + parenthesised trunk prefix

On top of format variation, several structural properties make regex harder:

Country codes are optional
Local format omits them entirely. +1 for US is often dropped even in international contexts.
Separators are unpredictable
Spaces, hyphens, dots, and parentheses all appear — sometimes mixed in the same number.
Digit counts vary by country
A valid phone number can have as few as 7 digits (some Pacific islands) or as many as 15 (E.164 maximum).
Extensions exist
ext 123, x4567, or #8 appended after the main number. Easy to forget and hard to make optional cleanly.
Leading zeros shift meaning
07911 in UK local format maps to +447911 internationally — the leading 0 is a trunk prefix, not part of the subscriber number.
Zip codes look like numbers
Five-digit US ZIP codes satisfy any pattern that allows 5 consecutive digits. Anchoring matters enormously.

The practical takeaway: use the narrowest pattern that your data actually requires. A strict E.164 pattern is far easier to reason about than a pattern trying to handle every world format simultaneously.

E.164 Format — The Safe Choice

E.164 is the ITU standard for international phone numbers: a leading +, then 1–3 digit country code, then the subscriber number, with no spaces or separators — 8 to 15 digits total after the +. This is what Twilio, Stripe, and most telephony APIs expect.

^\+[1-9]\d{7,14}$
^
Start anchor
Must match from position zero — prevents a valid number embedded inside a longer string from matching.
\+
Literal plus sign
The + is mandatory in E.164. Note: inside a character class [+] the backslash is not needed, but outside it is — \+ is clearest.
[1-9]
First digit of country code
Country codes never start with 0 (reserved) so we exclude it explicitly. This also rejects +0... junk.
\d{7,14}
Remaining digits
E.164 allows up to 15 total digits. With one leading digit already matched, we need 7–14 more. The minimum of 7 avoids matching very short digit strings.
$
End anchor
Must match to end of string. Without this, +12025550123garbage would pass.
// JavaScript
const E164 = /^\+[1-9]\d{7,14}$/;

E164.test('+12025550123')   // ✓ US number
E164.test('+447911123456')   // ✓ UK mobile
E164.test('+85291234567')    // ✓ Hong Kong
E164.test('+1 202 555 0123')  // ✗ spaces not allowed
E164.test('12025550123')     // ✗ missing leading +
E164.test('+0123456789')     // ✗ country code starts with 0
# Python
import re

E164 = re.compile(r'^\+[1-9]\d{7,14}$')

def is_e164(phone: str) -> bool:
    return bool(E164.match(phone))

# Use for: API payloads, database storage, Twilio/SMS sends
# Reject at the form level and prompt user to include country code

When to use E.164: any time you control the input — API payloads, database storage, message sending. Reject non-E.164 at the form level and show a helper prompt ("Include country code, e.g. +1 202 555 0123").

US and Canada Pattern

NANP numbers (North American Numbering Plan) cover the US, Canada, and several Caribbean countries. The format is (NXX) NXX-XXXX where N is 2–9. A permissive but correct pattern:

^(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$
(\+1[-.\s]?)?
Optional country code
The +1 prefix is optional — many US users omit it. [-.\ s]? allows a single separator after the country code.
\(?
Optional opening paren
The ? makes the ( optional. Matches both (202) and 202 styles.
\d{3}
Area code
Three digits. Does not enforce the NXX rule (first digit 2–9) — acceptable for most applications.
\)?
Optional closing paren
Matches the closing parenthesis when present. Note: this independently optional from the opening — a stricter pattern would pair them with a group.
[-.\s]?
Optional separator
Allows a dash, dot, or single space between number segments. The ? makes it optional so 2025550123 (no separators) also passes.
\d{3}
Exchange code
The three-digit central office code.
[-.\s]?
Optional separator
Same separator pattern between exchange and subscriber number.
\d{4}$
Subscriber number
Four digits to end of string.

Test cases — these should all pass:

Input Result Notes
2025550123 ✓ pass No separators, no country code
202-555-0123 ✓ pass Dash separators
202.555.0123 ✓ pass Dot separators
(202) 555-0123 ✓ pass Parens + space + dash
+1 202 555 0123 ✓ pass E.164-style with spaces
+1-202-555-0123 ✓ pass E.164-style with dashes
555-0123 ✗ fail Missing area code (7 digits only)
1-800-555-01234 ✗ fail Too many digits
(202 555-0123 ✗ fail Unmatched open paren — actually passes! See note below.

Paren pairing caveat: because \(? and \)? are independently optional, (202 555-0123 (open paren, no close) passes. If this matters, use a conditional group: (\(?\d{3}\)?|\(\d{3}\)) — or just normalise input by stripping all non-digits before validating.

// JavaScript
const US_PHONE = /^(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$/;

# Python
US_PHONE = re.compile(r'^(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}$')

UK Pattern

UK numbers are more complex than US numbers. Area codes have variable length — London uses a 2-digit area code (020) with 8-digit local numbers, while many rural areas use 5-digit area codes with 6-digit local numbers. The total digit count is always 10 (excluding the leading trunk prefix 0 or country code +44).

^(\+44\s?|0)(\d\s?){10}$

Breaking it down:

(\+44\s?|0)
Prefix
Either the international prefix +44 (with optional space) or the domestic trunk prefix 0. Exactly one is required.
(\d\s?){10}
10 digit-pairs
Ten repetitions of: one digit followed by an optional space. This allows spaces anywhere within the number body, which is how UK numbers are naturally written (e.g., 020 7946 0958).
$
End anchor
No trailing characters.
// JavaScript
const UK_PHONE = /^(\+44\s?|0)(\d\s?){10}$/;

UK_PHONE.test('07911123456')      // ✓ mobile, no spaces
UK_PHONE.test('07911 123456')     // ✓ mobile, one space
UK_PHONE.test('020 7946 0958')    // ✓ London landline
UK_PHONE.test('+44 7911 123456')  // ✓ international prefix
UK_PHONE.test('+447911123456')    // ✓ strict E.164
UK_PHONE.test('7911123456')       // ✗ missing prefix (0 or +44)
UK_PHONE.test('+44 (0)7911 123456') // ✗ parenthesised trunk prefix not matched

The +44 (0)7911 123456 style with a parenthesised trunk prefix is technically incorrect (the 0 is redundant when dialling internationally) but appears in print and on business cards. If you need to handle it, add a third alternative to the prefix group:

^(\+44\s?(\(0\)\s?)?|0)(\d\s?){10}$

Loose International Validator

When you need to accept phone numbers from any country without maintaining country-specific patterns, use a permissive pattern that rejects clearly non-phone strings while accepting the full range of international formats:

^\+?[\d\s\-().]{7,20}$
\+?
Optional plus
The + is optional so local-format numbers (without country code) pass. If you require it, remove the ?.
[\d\s\-().]{7,20}
Allowed characters
Digits, spaces, hyphens, dots, parentheses — every separator seen in real phone numbers. Minimum 7 characters (shortest real subscriber numbers) and maximum 20 (E.164 max 15 digits + some separators).
$
End anchor
Required when validating a standalone field — omit when extracting from free text.

When to use this vs strict patterns: use the loose pattern only as a pre-filter when you cannot know the user's country. Always follow it with a digit-count check after stripping non-digits (must be 7–15 digits). Never use it as your only validation layer — it will pass ZIP codes and other short digit strings if you forget the minimum-length constraint.

# Python: two-stage validation
LOOSE = re.compile(r'^\+?[\d\s\-().]{7,20}$')

def is_valid_international(phone: str) -> bool:
    if not LOOSE.match(phone.strip()):
        return False
    digits = re.sub(r'\D', '', phone)
    return 7 <= len(digits) <= 15  # E.164 max

Validation vs Extraction

There is a fundamental difference between validating a phone number field (the entire string must be a number) and extracting phone numbers from free text (find all numbers inside a paragraph). The patterns and anchoring requirements differ completely.

Validation (form field)
  • Always use ^ and $ anchors
  • Strict pattern — reject ambiguous input
  • Test with .test() / re.match()
  • Return true/false
Extraction (free text)
  • No anchors — or use word boundaries \b
  • Permissive pattern — catch varied formats
  • Use matchAll() / re.findall()
  • Post-process results to normalise

Here is how to extract all phone numbers from a paragraph of text:

// JavaScript — extract all phone numbers from free text
const text = `Call us on +1 (202) 555-0123 or reach support at
  +44 7911 123456. Emergency line: 0800-111-4567.`;

// Pattern: must start and end with a digit, allows separators in between
const EXTRACT = /\+?[0-9][0-9\s\-().]{5,18}[0-9]/g;

const found = [...text.matchAll(EXTRACT)].map(m => m[0].trim());
// → ['+1 (202) 555-0123', '+44 7911 123456', '0800-111-4567']
# Python — extract all phone numbers from free text
import re

text = """Call us on +1 (202) 555-0123 or reach support at
+44 7911 123456. Emergency line: 0800-111-4567."""

EXTRACT = re.compile(r'\+?[0-9][0-9\s\-().]{5,18}[0-9]')

found = [m.group().strip() for m in EXTRACT.finditer(text)]
# → ['+1 (202) 555-0123', '+44 7911 123456', '0800-111-4567']

Common Mistakes

1. Forgetting anchors for validation

Without ^ and $, the pattern matches anywhere inside the input — a 200-character garbage string that happens to contain 10 consecutive digits will pass.

// Wrong — no anchors
const BAD = /\d{10}/;
BAD.test('not a phone: 1234567890 (found!)'); // true — should be false

// Correct — anchored
const GOOD = /^\d{10}$/;
GOOD.test('not a phone: 1234567890 (found!)'); // false
2. Escaping + inside character classes

Outside a character class, + is a quantifier meaning "one or more". You must escape it as \+ to match a literal plus sign. Inside a character class [+], the backslash is not required but does no harm.

// Wrong — unescaped + is a quantifier, not a literal
const BAD = /^+\d{10}/;  // SyntaxError: nothing to repeat

// Correct
const GOOD = /^\+\d{10}/;  // matches +1234567890
3. Not accounting for extensions

Business numbers often include an extension: +1 202 555 0123 ext 456 or +1-202-555-0123 x456. A pattern without an optional extension group will reject these valid numbers.

# Add an optional extension group
US_WITH_EXT = re.compile(
    r'^(\+1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}'
    r'(\s*(ext|x|#)\.?\s*\d{1,6})?$',
    re.IGNORECASE
)
4. Matching ZIP codes by accident

Any pattern that allows 5 consecutive digits without a minimum total length will match US ZIP codes. Always enforce a digit count of at least 7 after stripping non-digits.

// Wrong — matches ZIP codes
const BAD = /\+?[\d\s\-]{5,}/;
BAD.test('90210');   // true — that's a ZIP code

// Better — require 7+ digits after stripping
const hasEnoughDigits = s => (s.replace(/\D/g, '').length >= 7);
hasEnoughDigits('90210');   // false
hasEnoughDigits('555-0123'); // true (7 digits)

Normalisation After Matching

Once you have a match, the next step is almost always normalisation — strip all non-digit characters, then reformat to E.164 for consistent storage. These one-liners handle the most common case (US numbers without a country code):

# Python — normalise to E.164
import re

def to_e164(phone: str, default_country: str = '1') -> str:
    digits = re.sub(r'\D', '', phone)          # strip everything non-digit
    if len(digits) == 10:                          # bare US/Canada number
        digits = default_country + digits
    if not (8 <= len(digits) <= 15):
        raise ValueError(f"Unexpected digit count: {len(digits)}")
    return f'+{digits}'

to_e164('(202) 555-0123')    # → '+12025550123'
to_e164('+44 7911 123456')   # → '+447911123456'
to_e164('07911 123456', '44') # → '+447911123456' (UK local)
// JavaScript — normalise to E.164
function toE164(phone, defaultCountry = '1') {
  const digits = phone.replace(/\D/g, '');
  const normalised = digits.length === 10
    ? defaultCountry + digits
    : digits;
  if (normalised.length < 8 || normalised.length > 15)
    throw new Error(`Unexpected digit count: ${normalised.length}`);
  return `+${normalised}`;
}

toE164('(202) 555-0123')    // → '+12025550123'
toE164('+44 7911 123456')   // → '+447911123456'

The normalisation step makes downstream processing much simpler — deduplication, database lookups, and SMS sending all become trivial when every stored number is in the same canonical format.

Test Your Phone Number Pattern

Paste any pattern from this guide and your test numbers into the free regex tester — runs entirely in your browser, nothing is uploaded.

Open Regex Tester →

Related