Validating Canadian Postal Codes: Regex, Edge Cases and Common Mistakes

The obvious Canadian postal code regex is wrong. It permits letters that never occur and rejects real codes like M4W and H2Z. Here is the correct pattern, the test cases that catch the usual failures, and what validation can and cannot prove.

The correct regular expression for a Canadian postal code is:

^[A-CEGHJ-NPR-TVXY]\d[A-CEGHJ-NPR-TV-Z] ?\d[A-CEGHJ-NPR-TV-Z]\d$

Note that the first character class and the other two are different. That asymmetry is the entire point, and it is what almost every implementation gets wrong. The pattern most developers reach for permits six letters that never appear in a Canadian postal code, and — depending on how it was written — often rejects two letters that appear in thousands of real ones.

This guide shows the failures in order of how often they occur, gives a test suite that catches all of them, and is clear about what structural validation can and cannot tell you.

The postal codes in these examples. Each one pairs a real forward sortation area — the first three characters, which carry the geography — with 0A0 as a placeholder for the last three. That is deliberate, and it is the same point the guide makes elsewhere: only the first three characters support any inference. The last three identify a delivery unit and tell you nothing you can reason from. No example here is a real delivery point, and none is meant to be posted to.> The exception is the table of deliberately invalid codes below, where the last three characters are part of what makes each one fail.

Why the obvious regex is wrong

Here is the pattern that appears in most answers, tutorials and copied snippets:

^[A-Z]\d[A-Z] ?\d[A-Z]\d$

It has the shape right — letter, digit, letter, optional space, digit, letter, digit — and it will accept every real Canadian postal code. The problem is what else it accepts.

Six letters never appear anywhere in a Canadian postal code: D, F, I, O, Q and U. They were excluded because they are too easily confused with other characters — I with the digit 1, O and Q with the digit 0 — by both human sorters and optical scanning equipment. [A-Z] permits all six, so this pattern happily validates D1F 1O1, which cannot exist.

That is the permissive failure. The restrictive one is worse, because it rejects paying customers.

The mistake that rejects real addresses

Developers who know about the excluded letters often write this:

# Still wrong — and this one loses you customers
^[ABCEGHJKLMNPRSTVXY]\d[ABCEGHJKLMNPRSTVXY] ?\d[ABCEGHJKLMNPRSTVXY]\d$

The character class is now correct — for the first position only. It has been applied to all three letter positions, and that is a different rule.

W and Z are excluded from the first position only. No postal district begins with either letter. But both are perfectly valid in the third and fifth positions, and real Canadian postal codes use them: 64 forward sortation areas carry W in the third position, and 49 carry Z. The pattern above rejects every address in both.

This failure is invisible in testing unless someone thought to include a W or Z code in the fixtures, and it fails silently in production — the customer sees "invalid postal code", assumes they mistyped, tries again, and eventually leaves.

PositionTypeLetters permittedExcluded
1LetterA B C E G H J K L M N P R S T V X YD F I O Q U W Z
2Digit0–9—
3LetterA B C E G H J K L M N P R S T V W X Y ZD F I O Q U
4Digit0–9—
5LetterA B C E G H J K L M N P R S T V W X Y ZD F I O Q U
6Digit0–9—

The correct pattern, explained

^[A-CEGHJ-NPR-TVXY]\d[A-CEGHJ-NPR-TV-Z] ?\d[A-CEGHJ-NPR-TV-Z]\d$

Reading the two character classes:

First position — [A-CEGHJ-NPR-TVXY]

RangeLetters
A-CA B C
EE
GHG H
J-NJ K L M N
PP
R-TR S T
VV
XYX Y

Eighteen letters — the eighteen postal district letters, no more.

Third and fifth positions — [A-CEGHJ-NPR-TV-Z]

Identical except the final range is V-Z rather than V and XY, which adds W and Z. Twenty letters.

The ? makes the space optional, which is usually what you want for input validation. Whether you store the space is a separate decision — see normalisation below.

If your language supports named patterns

Readability beats cleverness here, because the next person to read this regex will be tempted to "simplify" the two character classes into one. A comment or a named constant that says why they differ is worth more than the characters it costs:

FIRST_LETTER  = "[A-CEGHJ-NPR-TVXY]"      # 18 postal district letters; no W or Z
INNER_LETTER  = "[A-CEGHJ-NPR-TV-Z]"      # 20 letters; W and Z are valid here (M4W, H2Z)

Normalise before you validate

Validation failures are frequently input-handling failures wearing a costume. Normalise first:

  1. Trim leading and trailing whitespace. Pasted values carry it constantly.
  2. Uppercase. Users type lower case; the pattern above is uppercase-only. Either uppercase the input or add a case-insensitive flag — not both, and not neither.
  3. Collapse internal whitespace to a single space, or remove it entirely. Some users type two spaces, some type none.
  4. Strip lookalike Unicode. A non-breaking space pasted from a PDF or a web page is not a regular space and will fail a naive pattern. So will a Cyrillic character that renders identically to a Latin one.

Only then apply the pattern. A large share of "the form rejected my valid postal code" reports are a non-breaking space, not a wrong code.

What validation proves, and what it does not

Structural validation proves the format is possible. It does not prove the code exists.

A code can pass every structural check on this page and never have been assigned. Structure is a statement about shape, not about service: a well-formed code may be unassigned, or may have been assigned and since retired as delivery routes change.

If you need to know whether a code is real and currently in service, that requires the Canada Post postal code database, which is a licensed commercial product. No regex substitutes for it, and no free third-party mirror can legally provide it. What you can and can't do with postal code lookup covers the licensing question properly.

For most applications, structural validation at input plus address verification at the point it matters — shipping, billing — is the right split. Validating existence on every keystroke is expensive and rarely necessary.

What you cannot infer

This site's postal code validator implements exactly this split — format and region-consistency, never existence — and returns both regions for an X code rather than guessing one. Its page covers the tool-side edge cases in more detail than belongs here.

A test suite that catches the real failures

Every value below is chosen to exercise a specific rule. The valid examples are structurally valid — they follow every rule on this page — and are used here to test the pattern, not asserted to be live delivery points.

Must pass

InputTests
M5V 0A0Ordinary urban code
M4W 0A0W in position 3 — the regression that loses customers
H2Z 0A0Z in position 3 — same failure, different letter
X0A 0A0Rural code, second character 0
M5V0A0No space, if your rule allows it
m5v 0a0Lower case, after normalisation
X1A 0A0The only urban-marked X code — Yellowknife, in the Northwest Territories
X0B 0A0The X district, shared by NT and NU

Must fail

InputTests
D1A 1A1Excluded letter D in position 1
F1A 1A1Excluded letter F
I1A 1A1Excluded letter I
O1A 1A1Excluded letter O
Q1A 1A1Excluded letter Q
U1A 1A1Excluded letter U
W1A 1A1`W` in position 1 — valid later, never first
Z1A 1A1`Z` in position 1 — same
M5D 0A0Excluded letter D in position 3
M5V 0AToo short
M5V 0A00Too long
90210US ZIP code
M5V 0A0Two spaces, if not normalised first
5MV 0A0Letter and digit transposed

The eight "must fail" rows covering D, F, I, O, Q, U, W and Z are the ones worth writing first. They are the difference between a pattern that looks right and one that is right.

Where to validate

Client side, for feedback. Immediate, forgiving, and never the only check. Uppercase as the user types if you like, but do not block keystrokes — people paste, and people type out of order.

Server side, always. Client-side validation is a convenience, not a control. Anything reaching your database should have passed the same pattern on the server.

Not in the database schema alone. A CHECK constraint is a reasonable backstop but a poor primary validator — it produces an error message no end user should ever see.

Getting test data

You need addresses that exercise these rules, and you should not get them from your production database. Real customer addresses in a test environment are personal information sitting somewhere that was never designed to hold it, which is a live obligation under Canada's federal privacy law rather than a matter of taste. Synthetic address data and privacy covers the reasoning.

A seeded generator is the usual answer: the same input produces the same address every time, so a failing test is reproducible rather than a one-off you can never recreate. The Canadian address generator produces structurally valid synthetic addresses, and generating test addresses for Canada covers how to fold them into a test suite.

Frequently asked questions

What is the regex for a Canadian postal code?

^[A-CEGHJ-NPR-TVXY]\d[A-CEGHJ-NPR-TV-Z] ?\d[A-CEGHJ-NPR-TV-Z]\d$ — note that the first character class excludes W and Z while the other two include them.

Why does my postal code validator reject M4W?

Because it applies the first-position letter rule to every letter position. W is excluded only from the first character. M4W is one of 64 forward sortation Area, and rejecting it is a bug.

Which letters are not used in Canadian postal codes?

D, F, I, O, Q and U never appear in any position. W and Z never appear in the first position but are valid in the third and fifth.

Should the space be required?

For input validation, no — make it optional and normalise. For display and for printed mail, yes: Canada Post specifies one space between the two halves. How to write a Canadian address covers the formatting rules.

Does a valid format mean the postal code exists?

No. Structural validity is a statement about the shape of a code, not about whether Canada Post has assigned it or still routes mail to it. Confirming that a code exists and is in service requires Canada Post's licensed database.

Can I check that the postal code matches the province?

The first letter maps to a province or region, so a cross-check is possible. Treat a mismatch as a warning rather than a hard rejection — autofill, recent moves and shared-service addresses all produce legitimate mismatches, and a hard block costs you more valid users than it catches bad data.

Should I validate Canadian and US codes with the same rule?

No. This is the single most common cause of international checkout failures. Select the validation rule from the selected country. Postal code vs ZIP code covers the difference.

In short

Two character classes, not one. Eighteen letters in the first position, twenty in the third and fifth. Normalise before matching. Test with M4W and H2Z specifically, because a pattern that fails those fails quietly and expensively.

And remember what the check is worth: structural validity means the code is possible, never that it is real.

Tools and reference pages on this site

Related guides

Claims register (for CLAIMS.md)

#ClaimSource status
37D, F, I, O, Q, U never appear in any positionVerified earlier in project
38W and Z excluded from position 1 onlyVerified earlier in project
39M4W and H2Z are real Forward Sortation AreasVerified earlier in project
4018 valid first letters; 20 valid third/fifth lettersDerived from 37 and 38 — arithmetic, not a new claim
41Exclusion rationale is optical/visual confusionWidely documented; secondary sourcing only — keep the wording general
42Canada Post revises postal codes over timeReasonable and widely stated; primary source not attached
43Full postal code database is a licensed Canada Post productVerified earlier in project
44Canada Post specifies one space between halvesCanada Post addressing guidelines — verified
—All example codes in the test tablesLabelled structurally valid, not asserted as live delivery points

Claims 41 and 42 are stated generally in the article precisely because they lack primary sources. Do not sharpen either into a specific assertion without one.

Related