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.
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.
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.
| Position | Type | Letters permitted | Excluded |
|---|---|---|---|
| 1 | Letter | A B C E G H J K L M N P R S T V X Y | D F I O Q U W Z |
| 2 | Digit | 0–9 | — |
| 3 | Letter | A B C E G H J K L M N P R S T V W X Y Z | D F I O Q U |
| 4 | Digit | 0–9 | — |
| 5 | Letter | A B C E G H J K L M N P R S T V W X Y Z | D F I O Q U |
| 6 | Digit | 0–9 | — |
^[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]
| Range | Letters |
|---|---|
| A-C | A B C |
| E | E |
| GH | G H |
| J-N | J K L M N |
| P | P |
| R-T | R S T |
| V | V |
| XY | X 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.
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)
Validation failures are frequently input-handling failures wearing a costume. Normalise first:
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.
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.
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.
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.
| Input | Tests |
|---|---|
| M5V 0A0 | Ordinary urban code |
| M4W 0A0 | W in position 3 — the regression that loses customers |
| H2Z 0A0 | Z in position 3 — same failure, different letter |
| X0A 0A0 | Rural code, second character 0 |
| M5V0A0 | No space, if your rule allows it |
| m5v 0a0 | Lower case, after normalisation |
| X1A 0A0 | The only urban-marked X code — Yellowknife, in the Northwest Territories |
| X0B 0A0 | The X district, shared by NT and NU |
| Input | Tests |
|---|---|
| D1A 1A1 | Excluded letter D in position 1 |
| F1A 1A1 | Excluded letter F |
| I1A 1A1 | Excluded letter I |
| O1A 1A1 | Excluded letter O |
| Q1A 1A1 | Excluded letter Q |
| U1A 1A1 | Excluded letter U |
| W1A 1A1 | `W` in position 1 — valid later, never first |
| Z1A 1A1 | `Z` in position 1 — same |
| M5D 0A0 | Excluded letter D in position 3 |
| M5V 0A | Too short |
| M5V 0A00 | Too long |
| 90210 | US ZIP code |
| M5V 0A0 | Two spaces, if not normalised first |
| 5MV 0A0 | Letter 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.
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.
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.
^[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.
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.
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.
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.
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.
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.
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.
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
| # | Claim | Source status |
|---|---|---|
| 37 | D, F, I, O, Q, U never appear in any position | Verified earlier in project |
| 38 | W and Z excluded from position 1 only | Verified earlier in project |
| 39 | M4W and H2Z are real Forward Sortation Areas | Verified earlier in project |
| 40 | 18 valid first letters; 20 valid third/fifth letters | Derived from 37 and 38 — arithmetic, not a new claim |
| 41 | Exclusion rationale is optical/visual confusion | Widely documented; secondary sourcing only — keep the wording general |
| 42 | Canada Post revises postal codes over time | Reasonable and widely stated; primary source not attached |
| 43 | Full postal code database is a licensed Canada Post product | Verified earlier in project |
| 44 | Canada Post specifies one space between halves | Canada Post addressing guidelines — verified |
| — | All example codes in the test tables | Labelled 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.