Regular Expressions Module 10 – Validation patterns and their limits
Validating formats: numbers, IDs and dates
Write regex validators that reject near misses: whole-input anchors, numeric ranges, structured IDs, check digits in code and dates by shape plus calendar.
What you will learn
- Validate the whole input and refuse near misses, with a length cap and whole-input anchors chosen for the engine
- Choose a numeric range by alternation or by code, and recognise when alternation stops reading as a pattern
- Write patterns for IFSC and PIN codes from their official definitions, and tell what a shape check cannot know
- Combine a shape pattern with a check digit computed in code, such as the Luhn digit of a card number
- Distinguish `[0-9]` from `\d` for an ID, and know which script digits each one matches in each engine
Before you start
What you’ll learn · Before you start
What you will learn
- Validate the whole input and refuse near misses, with a length cap and whole-input anchors chosen for the engine
- Choose a numeric range by alternation or by code, and recognise when alternation stops reading as a pattern
- Write patterns for IFSC and PIN codes from their official definitions, and tell what a shape check cannot know
- Combine a shape pattern with a check digit computed in code, such as the Luhn digit of a card number
- Distinguish `[0-9]` from `\d` for an ID, and know which script digits each one matches in each engine
Before you start
On this page
A sign-up form accepted an 11-character IFSC with a stray newline on the end: the pattern ^[A-Z]{4}0[A-Z0-9]{6}$
compiled with Python’s re returned a match, so a 12-character string went into the database. The problem turned up
a week later, when a bank transfer failed on the extra character. The pattern was right; its anchors were the
wrong ones for a validator.
This lesson writes validators that reject what they must. The anchors have to cover the whole input and refuse a trailing newline, a length cap goes before the pattern, numeric ranges sometimes need code, structured identifiers are written from their official definitions, check digits are a loop and not a pattern, and a date is a question about shape and a question about the calendar at the same time.
Validate the whole input, and check its length first
A search answers “is there a match somewhere”; a validator answers “does the whole input match”. For Python the
tested call is re.fullmatch, which the module documentation describes as “checks for entire string to be a
match”. For JavaScript the pattern wraps the body with ^ and $ and runs without the m flag. In Go, and in
Python from 3.14, the pair \A and \z holds only at the very start and the very end of the string, so no newline
slips past it. The run below checks the IFSC pattern on the valid code and three near misses with four calls,
re.fullmatch, a hand-rolled ^…$, \A…\z and re.search, so that the trap is visible:
"""Validate the whole input: fullmatch, ^...$ and the trailing-newline trap.
The pattern comes from the RBI's FAQs on NEFT: an IFSC is 11 characters, four
letters, the digit 0, and six letters or digits. The run compares three Python
calls on the same list of inputs so the trap on the fourth row is visible.
"""
import re
pattern = r"[A-Z]{4}0[A-Z0-9]{6}"
anchored = r"^[A-Z]{4}0[A-Z0-9]{6}$"
hard = r"\A[A-Z]{4}0[A-Z0-9]{6}\z"
inputs = [
"SBIN0000691", # the real code of SBI's Delhi main branch
"SBIN0000691X", # one letter too many
"foo SBIN0000691 bar", # the code buried inside a longer string
"SBIN0000691\n", # the trailing-newline trap
]
def answer(call, label):
print(f"{label}:")
for s in inputs:
print(f" {s!r:30} -> {bool(call(s))}")
print("re.fullmatch of the body")
answer(lambda s: re.fullmatch(pattern, s), "fullmatch")
print()
print("re.match with ^...$ (default flags): $ lets a final newline through")
answer(lambda s: re.match(anchored, s), "re.match with ^...$")
print()
print(r"re.match with \A...\z (Python 3.14): both ends are hard")
answer(lambda s: re.match(hard, s), r"re.match with \A...\z")
print()
print("re.search (not a validator): a match anywhere passes")
answer(lambda s: re.search(pattern, s), "re.search") Output
re.fullmatch of the body fullmatch: 'SBIN0000691' -> True 'SBIN0000691X' -> False 'foo SBIN0000691 bar' -> False 'SBIN0000691\n' -> False re.match with ^...$ (default flags): $ lets a final newline through re.match with ^...$: 'SBIN0000691' -> True 'SBIN0000691X' -> False 'foo SBIN0000691 bar' -> False 'SBIN0000691\n' -> True re.match with \A...\z (Python 3.14): both ends are hard re.match with \A...\z: 'SBIN0000691' -> True 'SBIN0000691X' -> False 'foo SBIN0000691 bar' -> False 'SBIN0000691\n' -> False re.search (not a validator): a match anywhere passes re.search: 'SBIN0000691' -> True 'SBIN0000691X' -> True 'foo SBIN0000691 bar' -> True 'SBIN0000691\n' -> True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 whole_input.py
The first and third blocks accept and reject the same strings; the fourth row of the second block is the
trailing-newline trap: A validation bug: a pattern such as ^abc$ accepts abc followed by a line feed in engines whose $ also holds before a final newline, so the checked value still carries the extra character. More in “Anchors: start, end and multiline mode”, where $ holds right before a final line feed and so
lets SBIN0000691\n pass. The anchors lesson, Anchors and line modes, owns
the trap in full and shows the four combinations with the m flag. In JavaScript, ^ and $ without the m flag
already hold only at the very start and end, as \A and \z do in Python, and Go’s $ without the m flag does the
same: RE2’s syntax page describes it as “like \z not \Z”.
Writing the validator with search
Wrong: calling re.search(r"[A-Z]{4}0[A-Z0-9]{6}", value) and treating a truthy result as valid. search is happy
with a match anywhere, so foo SBIN0000691 bar passes and so does the 12-character string with a letter stuck on.
Right: re.fullmatch(r"[A-Z]{4}0[A-Z0-9]{6}", value), which has to cover every character.
Before the pattern comes a length cap. OWASP’s Input Validation Cheat Sheet sets a rule for each kind of field, and for a string it asks for “length limits and the characters or structure required by the field”. Saying what a field accepts and refusing everything else is allowlist validation: A rule that says what a field accepts (length, allowed characters, shape) and refuses everything else, instead of listing values to block. OWASP's Input Validation Cheat Sheet treats it as the default., the approach the cheat sheet prefers, and even for an open comment field it keeps the length limited. A cap on the raw bytes keeps an attacker’s million-character payload from reaching the regex engine and bounds the matcher’s worst case before the pattern’s own shape is analysed. For an IFSC the cap is 11 bytes; for a comment it is wider but still finite.
A good shape for a validator is layers tried in order: a length cap, a shape pattern, a numeric range check, a check digit, and finally an existence lookup. The sections below each build one layer, and the figure steps through all five in order, with one near miss that the shape layer stops:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- A field value arrives. A user types or pastes a value. The validator says yes or no to this one string; a near miss here is quiet, so the layers below are each a chance to refuse what the next layer could not catch.
- Length cap first. OWASP's rule for a string field starts with length limits. A cap bounds the matcher's worst case; for an IFSC it is 11 bytes. A value above the cap fails here and never reaches the pattern; this one has 11 bytes and goes on.
- Shape pattern. The pattern reads the official rule as its text. For an IFSC it is four letters, the digit 0, and six letters or digits, wrapped by whole-input anchors. This value has the letter O where the digit 0 belongs, so it fails here, as it did in all three languages of the IFSC run.
- Range check. A numeric field can have a range that a pattern states badly, such as 1 to 65535. The pattern bounds the shape; a comparison in code checks the value.
- Check digit. For a card-like number, an IMEI or an ISBN, a short loop over the digits confirms the check digit. A pattern for the same sum would be too long to read, so the loop runs after the shape passes.
- Existence lookup. A shape check says nothing about whether the IFSC names a real branch or the PIN code names a real post office. The last layer looks up the value in a dataset: razorpay/ifsc for the IFSC and the India Post directory for the PIN code.
Checkpoint
Check: Whole-input anchors and the length cap
Integers, decimals and ranges: when alternation stops being worth it
A regex can keep leading zeros out, allow an optional sign and limit the decimal part to two digits — these are shape questions and a pattern reads cleanly on them. A number from one up to a few hundred has a shape too, but at some point the alternatives stop teaching the reader what the rule is. The example below checks an integer with an optional minus sign and no leading zeros, then an amount with at most two digits after the point, then an octet from 0 to 255 as an alternation: The pattern operator written as a vertical bar: it lets a pattern match any one of two or more alternatives. The bar has the lowest precedence of every operator, so it splits the whole pattern at the top level. More in “Alternation and the order of choices” beside a five-line function that parses and compares, on the same fourteen inputs, and last a port number from 1 to 65535 written both ways:
"""Shape rules and numeric ranges.
Four blocks: a signed integer with no leading zeros, an amount with at most
two decimals, the octet rule 0 to 255 as an alternation printed beside a code
comparison, and a port rule for 1 to 65535 written as six branches, compared
with the code on every value up to 99999. Digits are written [0-9], the class
a validator reads the same way in every engine.
"""
import re
integer = re.compile(r"\A-?(?:0|[1-9][0-9]*)\z")
amount = re.compile(r"\A-?(?:0|[1-9][0-9]*)(?:\.[0-9]{1,2})?\z")
octet_pat = r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
octet = re.compile(rf"\A{octet_pat}\z")
port_pat = r"(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{0,3})"
port = re.compile(rf"\A{port_pat}\z")
print("Signed integer, no leading zeros")
for s in ["0", "-0", "12", "012", "-12", "-", "", "012345", "+1"]:
print(f" {s!r:10} -> {bool(integer.match(s))}")
print()
print("Amount with up to two decimals, no leading zeros")
for s in ["0", "12", "12.5", "12.50", "12.500", "-3.14", ".5", "1.", "0.5", "00.5"]:
print(f" {s!r:10} -> {bool(amount.match(s))}")
print()
print("Octet 0 to 255: alternation vs a code comparison")
def by_code(s: str) -> bool:
if not re.fullmatch(r"[0-9]{1,3}", s):
return False
if len(s) > 1 and s[0] == "0":
return False
return 0 <= int(s) <= 255
print(f" {'input':10} {'alternation':>14} {'by code':>10}")
for s in ["0", "9", "10", "99", "199", "200", "249", "255", "256", "300", "01", "099", "-0", ""]:
pat = bool(octet.match(s))
cod = by_code(s)
agree = "" if pat == cod else " (disagree)"
print(f" {s!r:10} {str(pat):>14} {str(cod):>10}{agree}")
print()
print("Port 1 to 65535: six branches vs a code comparison")
def port_by_code(s: str) -> bool:
if not re.fullmatch(r"[0-9]{1,5}", s) or s[0] == "0":
return False
return 1 <= int(s) <= 65535
values = [str(n) for n in range(100000)] + ["0" + str(n) for n in range(1000)]
wrong = [s for s in values if bool(port.match(s)) != port_by_code(s)]
print(f" {len(values)} inputs, 0 to 99999 and 0 to 999 with a leading 0: {len(wrong)} disagree")
print(f" {'input':10} {'six branches':>14} {'by code':>10}")
for s in ["0", "1", "9999", "10000", "59999", "60000", "65535", "65536", "99999", "080"]:
print(f" {s!r:10} {str(bool(port.match(s))):>14} {str(port_by_code(s)):>10}") Output
Signed integer, no leading zeros '0' -> True '-0' -> True '12' -> True '012' -> False '-12' -> True '-' -> False '' -> False '012345' -> False '+1' -> False Amount with up to two decimals, no leading zeros '0' -> True '12' -> True '12.5' -> True '12.50' -> True '12.500' -> False '-3.14' -> True '.5' -> False '1.' -> False '0.5' -> True '00.5' -> False Octet 0 to 255: alternation vs a code comparison input alternation by code '0' True True '9' True True '10' True True '99' True True '199' True True '200' True True '249' True True '255' True True '256' False False '300' False False '01' False False '099' False False '-0' False False '' False False Port 1 to 65535: six branches vs a code comparison 101000 inputs, 0 to 99999 and 0 to 999 with a leading 0: 0 disagree input six branches by code '0' False False '1' True True '9999' True True '10000' True True '59999' True True '60000' True True '65535' True True '65536' False False '99999' False False '080' False False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ranges.py
The first and second patterns stay readable, because the shape is the rule. The octet pattern
(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]) already needs four branches to say 0 to 255, and it agrees with the
code on every row. For a port from 1 to 65535 the same style needs six branches, from 6553[0-5] down to
[1-9][0-9]{0,3}. The run checked them against the comparison on 101000 inputs and found no disagreement, yet anyone
reading the pattern has to test every boundary by hand, while the code says 1 <= int(s) <= 65535. The
matcher’s job is the shape; the comparison belongs to code.
| Input | The octet pattern | 0 <= int(s) <= 255, no leading zero |
|---|---|---|
0 |
yes | yes |
9 |
yes | yes |
99 |
yes | yes |
199 |
yes | yes |
255 |
yes | yes |
256 |
no | no |
300 |
no | no |
01 |
no | no |
099 |
no | no |
Key idea
A pattern reads well on a shape rule (an optional sign, no leading zeros, at most two decimals) and on a short range such as 0 to 255, which still fits in four branches; for a wide range such as 1 to 65535, keep the shape as the regex and move the comparison into code.
Checkpoint
Check: Shape patterns and numeric ranges
IFSC and PIN codes from their official definitions
Two Indian formats carry their rules on their faces. The RBI’s FAQs on NEFT describe the Indian Financial System Code as an 11-character code whose “first 4 alpha characters” stand for the bank and whose last six stand for the branch, and add that “the 5th character is 0 (zero)”. The Department of Posts, in the Tamil Nadu circle’s note on the PIN code system, states that each PIN code has six digits and that the first digit indicates the zone, and its table lists eight zones, from 1 for Delhi, Haryana and Punjab to 8 for Bihar and Jharkhand. Codes that start with 9 are in use too, as the All India Pincode Directory that the PIN Code Finder reads shows, so the zone digit runs from 1 to 9. Those facts give the patterns:
- IFSC:
^[A-Z]{4}0[A-Z0-9]{6}$— four letters, the digit zero, then six letters or digits. - PIN code:
^[1-9][0-9]{5}$— a zone digit from 1 to 9, then five more digits.
The next run writes the IFSC validator in Python, JavaScript and Go, with the same near misses in each tab, and the Go tab also runs a loose search without the anchors to show the difference between finding a match and validating a field:
Python 3.14.8 · ifsc/ifsc.py
"""IFSC validator in Python 3.14.8.
The RBI's FAQs on NEFT give the shape: four letters for the bank, the digit 0,
and six characters for the branch. Run the pattern on the real code and on
five near misses.
"""
import re
ifsc = re.compile(r"\A[A-Z]{4}0[A-Z0-9]{6}\z")
cases = [
"SBIN0000691", # real code
"SBINA000691", # fifth character is a letter, not 0
"sbin0000691", # lower case
"SBIN00006911", # one character too many
"SBINO000691", # letter O instead of digit 0
"SBIN00006\u096f1", # a Devanagari digit in the branch part
"SBIN0000691\n", # a trailing newline
]
print(f" {'input':24} valid?")
for s in cases:
print(f" {s!r:24} {bool(ifsc.match(s))}") Output
input valid? 'SBIN0000691' True 'SBINA000691' False 'sbin0000691' False 'SBIN00006911' False 'SBINO000691' False 'SBIN00006९1' False 'SBIN0000691\n' False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ifsc.py
JavaScript (Node 24.21.0) · ifsc/ifsc.mjs
// IFSC validator in Node.js 24.21.0.
//
// JavaScript has no fullmatch; the pattern is wrapped with ^ and $ and runs
// without the m flag, so the dollar sign holds only at the end of the string.
const ifsc = /^[A-Z]{4}0[A-Z0-9]{6}$/;
const cases = [
"SBIN0000691", // real code
"SBINA000691", // fifth character is a letter, not 0
"sbin0000691", // lower case
"SBIN00006911", // one character too many
"SBINO000691", // letter O instead of digit 0
"SBIN00006\u096f1", // a Devanagari digit in the branch part
"SBIN0000691\n", // a trailing newline
];
console.log(" input valid?");
for (const s of cases) {
const shown = JSON.stringify(s);
console.log(` ${shown.padEnd(24)} ${ifsc.test(s)}`);
} Output
input valid? "SBIN0000691" true "SBINA000691" false "sbin0000691" false "SBIN00006911" false "SBINO000691" false "SBIN00006९1" false "SBIN0000691\n" false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node ifsc.mjs
Go 1.27.1 · ifsc/ifsc.go
// IFSC validator in Go 1.27.1.
//
// Go has no fullmatch call either; the anchors \A and \z hold only at the very
// start and end of the input, so a trailing newline fails. The find variant on
// the loose pattern shows what a search returns instead of a validator.
package main
import (
"fmt"
"regexp"
)
func main() {
ifsc := regexp.MustCompile(`\A[A-Z]{4}0[A-Z0-9]{6}\z`)
loose := regexp.MustCompile(`[A-Z]{4}0[A-Z0-9]{6}`)
cases := []string{
"SBIN0000691",
"SBINA000691",
"sbin0000691",
"SBIN00006911",
"SBINO000691",
"SBIN00006\u096f1",
"SBIN0000691\n",
"foo SBIN0000691 bar",
}
fmt.Printf(" %-24s valid? what loose.FindString would return\n", "input")
for _, s := range cases {
fmt.Printf(" %-24q %-6v %q\n", s, ifsc.MatchString(s), loose.FindString(s))
}
} Output
input valid? what loose.FindString would return "SBIN0000691" true "SBIN0000691" "SBINA000691" false "" "sbin0000691" false "" "SBIN00006911" false "SBIN0000691" "SBINO000691" false "" "SBIN00006९1" false "" "SBIN0000691\n" false "SBIN0000691" "foo SBIN0000691 bar" false "SBIN0000691"
Recorded with Go 1.27.1 on macOS 26 arm64. To run it yourself: go run ifsc.go
Every input the three tabs share ends the same way in all three languages: the valid code passes, and a letter in
the fifth place, a lower-case bank prefix, twelve characters, the letter O instead of the digit 0, a Devanagari digit
in the branch part and a trailing newline all fail. The Go tab’s last column is the loose search: without anchors,
FindString still pulls the eleven valid characters out of the twelve-character string, out of the one with the
newline and out of foo SBIN0000691 bar. The PIN code validator reads the same way; the browser example below runs
it and lets you edit the input without leaving the page:
// PIN code validator in your browser (QuickJS twin: parity with Node.js).
//
// From the Department of Posts: a PIN code has six digits, and the first digit
// indicates the postal zone. The pattern reads the sentence back: a zone digit
// from 1 to 9, then five more digits.
const pin = /^[1-9][0-9]{5}$/;
const cases = [
"400001", // Mumbai General Post Office
"110001", // Connaught Place, New Delhi
"011234", // leading zero: zone 0 does not exist
"40000", // five digits only
"7441012", // seven digits
"4 00001", // a stray space
"\u096A\u0966\u0966\u0966\u0966\u0967", // Devanagari digits
];
console.log(" input valid?");
for (const s of cases) {
console.log(` ${JSON.stringify(s).padEnd(14)} ${pin.test(s)}`);
} Output
input valid? "400001" true "110001" true "011234" false "40000" false "7441012" false "4 00001" false "४००००१" false
Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node pin.mjs
Runs on this device, in your browser. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs.
Your run, in this browser
A shape check: A pattern that decides whether a string is written in a given form, such as four letters, a 0 and six characters. It says nothing about whether the value exists; the existence check runs after it. says nothing about whether the code names a real branch or a delivery office. For that the lookup is against a dataset: the IFSC Code Finder reads the razorpay/ifsc list of branches, and the PIN Code Finder reads the Department of Posts’ All India Pincode Directory.
IFSC Code Finder & Validator Enter an IFSC after your pattern says the shape is fine; the finder tells you whether a branch has that code and shows its address. PIN Code Finder (India Post) The pattern accepts any six digits that start with 1 to 9; whether a code such as 400001 belongs to a real post office needs the directory.Where the names in this section come from
IFSC and PIN code are Indian formats defined by the Reserve Bank of India and the Department of Posts. The two tools linked above read Indian datasets. Readers outside India can apply the same recipe to their own codes: the shape from the issuer’s documentation and the existence check from an official register.
Checkpoint
Check: IFSC and PIN codes from their definitions
Check digits need code, not a pattern
Many identifiers end in a check digit: A digit at the end of a number computed from the digits before it, used to catch a single wrong digit and most swaps of two neighbouring digits when the number is read or typed by hand. A short loop computes it after a pattern has checked the shape. computed from the digits before it. A card-like sixteen-digit number uses the Luhn algorithm: A check-digit rule from ISO/IEC 7812-1 Annex B. Double every second digit from the right, add the digits of each doubled result to the rest, and the total must end in zero. Used for payment card numbers and IMEIs.: double every second digit from the right, add the digits of each doubled result, add the untouched digits, and the sum has to end in zero. ISO/IEC 7812-1 specifies the number format, and its Annex B gives the method. Could a pattern do the sum? In principle, yes: a matcher reading the sixteen digits only has to remember the running total modulo 10, and the position tells it which digits are doubled, so the strings that pass form a regular language: A set of strings that can be checked in one pass over the text with a fixed amount of memory. Regular expressions in the strict sense, without backreferences or recursion, describe these sets, so they cannot count how deeply things nest. More in “Emails, URLs and when to use a parser”. In practice nobody writes that pattern, because it would have to spell out every total at every position and nobody reading it could check it, while the loop in the run takes ten lines. The run checks the shape with a pattern and the digit with that loop, on five invented inputs:
"""Luhn check digit on an invented sixteen-digit number.
ISO/IEC 7812-1 Annex B gives the method. The run checks the shape with a
regex and the digit with a short loop, on five made-up strings. Card networks
are never asked about: this is a check-digit test, not a payment check.
"""
import re
shape = re.compile(r"\A[0-9]{16}\z")
def luhn_ok(digits: str) -> bool:
total = 0
for i, ch in enumerate(reversed(digits)):
n = int(ch)
if i % 2 == 1:
n *= 2
if n > 9:
n -= 9
total += n
return total % 10 == 0
cases = [
"4000000000000002", # shape passes, Luhn passes
"4000000000000003", # shape passes, Luhn fails (last digit off by one)
"4111111111111111", # shape passes, Luhn passes
"4000-0000-0000-0002", # shape fails: hyphens would need cleaning first
"0000000000000000", # Luhn is happy with all zeros; a real transaction is not
]
print(f" {'input':24} shape? luhn?")
for s in cases:
s_ok = bool(shape.match(s))
l_ok = s_ok and luhn_ok(s)
print(f" {s!r:24} {s_ok!s:6} {l_ok!s}") Output
input shape? luhn? '4000000000000002' True True '4000000000000003' True False '4111111111111111' True True '4000-0000-0000-0002' False False '0000000000000000' True True
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 luhn.py
The first and third pass the shape and the digit. The second differs from the first only in its last digit, so it passes the shape and fails the digit. The fourth is the first number written in groups of four, which the shape refuses before the digit is checked. The fifth, sixteen zeros, passes both, since zero doubled is still zero: a pass only says the digit is consistent, not that the account is real. MySmartCoPilot’s check-digit validator works the same sum one step at a time:
Card & Check-digit Validator Luhn is one rule of a family: GS1 for a product barcode, ISBN-10 and ISSN for modulus 11; the tool shows the sum step by step for each.Key idea
A shape pattern catches a wrong kind of character; a check digit catches a single mistyped digit and most swaps of two neighbouring digits, which every shape accepts, and it is a short loop that runs after the shape passes.
Checkpoint
Check: Check digits are code, not a pattern
[0-9] for IDs, because \d may mean more
In Python’s re with its default Unicode mode the shorthand class: A backslash escape that stands for a whole character class: \d for digits, \w for word characters and \s for white space. Which characters each one holds depends on the regex flavour and its flags. More in “Shorthand classes for digits, words and spaces” \d matches
every character of Unicode general category Nd, so a Devanagari ७ or a Tamil ௭ passes \d+. ECMAScript’s
specification restricts \d to the ASCII decimals, and Go’s regexp follows RE2 and does the same. For an identifier
such as a PIN code the pattern you want is [0-9]{6}: its class holds the ten characters 0 to 9 and nothing else, every regex engine reads it
the same way, and no flag changes its meaning. The run below tries five inputs, ASCII digits, two strings of
Devanagari digits, a mix of the two and an empty string, against four patterns side by side in Python:
"""Shorthand d against [0-9] on an ASCII string and a Devanagari string.
Python's re reads \\d as every character of Unicode general category Nd when
the Unicode flag is on (its default). ECMAScript and RE2 restrict \\d to the
ASCII decimals. For an identifier, [0-9] is the spelling that reads the same
in every engine.
"""
import re
ascii_digits = re.compile(r"\A\d+\z")
range_digits = re.compile(r"\A[0-9]+\z")
ascii_flag = re.compile(r"(?a:\A\d+\z)")
range_class = re.compile(r"\A[\u0030-\u0039]+\z") # the same ten characters
inputs = [
"123", # ASCII
"\u0967\u0968\u0969", # Devanagari 1 2 3
"\u096f\u096f\u096f", # Devanagari 9 9 9
"12\u09663", # mixed: ASCII, Devanagari 0, ASCII
"",
]
print(f" {'input':15} {'\\d+':>5} {'[0-9]+':>8} {'(?a:\\d+)':>10} {'[\\u0030-\\u0039]+':>18}")
for s in inputs:
print(
f" {s!r:15}"
f" {str(bool(ascii_digits.match(s))):>5}"
f" {str(bool(range_digits.match(s))):>8}"
f" {str(bool(ascii_flag.match(s))):>10}"
f" {str(bool(range_class.match(s))):>18}"
) Output
input \d+ [0-9]+ (?a:\d+) [\u0030-\u0039]+ '123' True True True True '१२३' True False False False '९९९' True False False False '12०3' True False False False '' False False False False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 digits.py
Shorthand classes covers which characters each engine’s \d, \s and \w holds,
and the inline flags that narrow them to ASCII. For a validator, pick the class that reads the same in every engine
you ship to, which for a decimal digit: A character of Unicode general category Nd: a digit 0 to 9 of some script, such as the ASCII 7, the Devanagari ७ or the Tamil ௭. \p{Nd} matches all of them; [0-9] matches only the ASCII ten. More in “Shorthand classes for digits, words and spaces” means [0-9], and leave the Unicode-aware
form for text where the extra digits belong.
Trusting \d to behave the same everywhere
Wrong: writing a six-digit PIN check as ^\d{6}$ and assuming it rejects everything that is not the ten ASCII
digits. In the run above, Python’s \d+ accepted every string of Devanagari digits, so १२३४५६ passes ^\d{6}$
under the default flags. Right: ^[0-9]{6}$ in every language, or (?a:\d{6}) in Python for the ASCII flag
inline.
Checkpoint
Check: [0-9] against the shorthand \d
Addresses and dates: a shape check, then the real check
An IPv4 address looks like four decimal octets separated by dots, each from 0 to 255. The pattern
(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9]) written four times with dots in between gets the shape right, and
its last branch also refuses an octet with a leading zero such as 01. Python’s ipaddress.ip_address reads the
string too, and since Python 3.9.5 it refuses 192.168.01.1: the module stopped tolerating leading zeros and now
raises an error for them, so the pattern and the parser agree on the near misses. The next run runs both on the same
ten values:
"""IPv4 shape by regex, real check by ipaddress.ip_address.
Python's ipaddress module refuses an octet with leading zeros since 3.9.5. The
dotted-quad regex refuses one too, so the two agree on the near misses. The run
prints every input against both.
"""
import ipaddress
import re
octet = r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])"
ipv4 = re.compile(rf"\A{octet}(?:\.{octet}){{3}}\z")
inputs = [
"0.0.0.0",
"127.0.0.1",
"192.168.1.1",
"255.255.255.255",
"256.0.0.1", # above 255 in the first octet
"192.168.01.1", # leading zero in the third octet
"1.2.3", # three octets only
"1.2.3.4.5", # five octets
"01.2.3.4", # leading zero in the first octet
" 192.168.1.1", # a stray leading space
]
def by_module(s: str) -> str:
try:
return str(ipaddress.ip_address(s))
except ValueError as e:
return f"refused ({e})"
print(f" {'input':24} regex ipaddress")
for s in inputs:
print(f" {s!r:24} {str(bool(ipv4.match(s))):<5} {by_module(s)}") Output
input regex ipaddress
'0.0.0.0' True 0.0.0.0
'127.0.0.1' True 127.0.0.1
'192.168.1.1' True 192.168.1.1
'255.255.255.255' True 255.255.255.255
'256.0.0.1' False refused ('256.0.0.1' does not appear to be an IPv4 or IPv6 address)
'192.168.01.1' False refused ('192.168.01.1' does not appear to be an IPv4 or IPv6 address)
'1.2.3' False refused ('1.2.3' does not appear to be an IPv4 or IPv6 address)
'1.2.3.4.5' False refused ('1.2.3.4.5' does not appear to be an IPv4 or IPv6 address)
'01.2.3.4' False refused ('01.2.3.4' does not appear to be an IPv4 or IPv6 address)
' 192.168.1.1' False refused (' 192.168.1.1' does not appear to be an IPv4 or IPv6 address)
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ipv4.py
A field for a day of the month, a month and a four-digit year is the same shape plus a calendar question. The pattern
([0-3][0-9])/([01][0-9])/([0-9]{4}) passes the shape, and datetime.date(year, month, day) tells you whether the
day exists in that month. A short list of cases says what the pattern decides and what datetime decides: day 29 of
month 2 passes in a leap year and fails outside one, day 31 of month 2 fails in any year, and a month of 13 fails
always.
"""Date field: shape by regex, calendar by datetime.date.
The pattern catches the three groups of digits; datetime.date decides whether
the three numbers name a real day. Both must say yes for the field to be
accepted.
"""
import datetime
import re
shape = re.compile(r"\A([0-3][0-9])/([01][0-9])/([0-9]{4})\z")
def check(s: str) -> str:
m = shape.match(s)
if not m:
return "shape fails"
day, month, year = (int(part) for part in m.groups())
try:
datetime.date(year, month, day)
except ValueError as e:
return f"shape ok, calendar fails: {e}"
return "shape ok, calendar ok"
# Inputs are a day, a month and a four-digit year joined by slashes.
inputs = [
"01/02/2024", # a real day in a leap year
"29/02/2024", # the leap day
"29/02/2023", # the same day in a non-leap year
"31/02/2026", # day 31 of month 2 does not exist
"31/04/2026", # day 31 of month 4 does not exist
"15/13/2026", # month 13 does not exist
"32/01/2026", # day 32 does not exist (shape still passes)
"99/99/9999", # a day and month beyond the shape
]
print(f" {'input':14} verdict")
for s in inputs:
print(f" {s!r:14} {check(s)}") Output
input verdict '01/02/2024' shape ok, calendar ok '29/02/2024' shape ok, calendar ok '29/02/2023' shape ok, calendar fails: day 29 must be in range 1..28 for month 2 in year 2023 '31/02/2026' shape ok, calendar fails: day 31 must be in range 1..28 for month 2 in year 2026 '31/04/2026' shape ok, calendar fails: day 31 must be in range 1..30 for month 4 in year 2026 '15/13/2026' shape ok, calendar fails: month must be in 1..12, not 13 '32/01/2026' shape ok, calendar fails: day 32 must be in range 1..31 for month 1 in year 2026 '99/99/9999' shape fails
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dates.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Emails and URLs are where a shape check stops being enough: the HTML Standard defines its own “valid email address” and calls that rule a “willful violation” of RFC 5322, and only a URL parser can say which host a URL names. Emails, URLs and when to use a parser walks that border. For an address or a date, the regex and the real check sit side by side, and even an IPv4 address has written forms that a browser reads but no dotted-quad pattern accepts:
IP Address Converter Paste 0x7f.1 or 2130706433 and see the address a browser reads in it, 127.0.0.1: one address, several written forms.Let a newline through the date shape
Press Edit on the dates run and add "29/02/2024\n" to the inputs. The shape is anchored with \A and \z, so the
line feed fails at the shape step. Then write ^ and $ in their place and run it again: the dollar sign lets the
line feed through, and the calendar check says yes as well, because it reads only the three groups of digits. A
newline that the shape lets in gets past the calendar check too.
A validator that cleans text data first (whitespace, no-break spaces, digit grouping) belongs to Cleaning text data with regex; it runs before the layers of this lesson.
Checkpoint
Check: IPv4 and dd mm yyyy by shape plus a real check
Practise: build an id and date validator
The exercise asks for four validators: an IFSC code, a PIN code, a dd slash mm slash yyyy date (shape plus
calendar), and an IPv4 address written as four decimal octets, each from 0 up to 255. The Python version has
ipaddress and datetime available; the JavaScript version has to check the octets and the calendar in code
itself.
Exercise · Medium · JavaScript, Python
Validators for an IFSC, a PIN code, a date field and an IPv4 address
Write four whole-input validators that reject near misses. Each takes a string and returns true or false (True or False in Python); no partial credit for a value with a stray newline or a leading space.
The four formats are the ones the lesson defines:
- IFSC: 11 characters, four letters for the bank, the digit 0 as the fifth character, then six letters or digits. From the RBI's FAQs on the Indian Financial System Code.
- PIN code: six digits, with a first digit from 1 to 9 (the postal zone, as the Department of Posts states).
- A date field in the shape day slash month slash four-digit year:
([0-3]\d)/([01]\d)/(\d{4})passes the shape, and the three numbers have to name a real day. Day 29 of month 2 passes only in a leap year. - IPv4 written as four decimal octets separated by dots, each from 0 to 255 with no leading zeros. In the Python version the standard library module
ipaddressis available; in the JavaScript version you check the octets and the calendar in code.
Keep the shape patterns in PATTERNS in each language; the tests read them to confirm that each one holds the rule, and that the digit class you use is [0-9] rather than the shorthand that Python widens to Unicode. The starter has stubs that return False, so every test fails until you write the body.
JavaScript · Starter code · validators.mjs
// validators.mjs: four validators that cover the whole input and reject near misses.
//
// Keep each shape pattern in PATTERNS: the tests read it. The real checks (the calendar for a date, the octet
// range for an IPv4 address) run in code after the shape passes.
// Starter patterns: fill in the four with the lesson's rules.
export const PATTERNS = {
ifsc: /(?:)/,
pin: /(?:)/,
date: /(?:)/,
ipv4: /(?:)/,
};
/** True when the whole value is an 11-character IFSC: four letters, the digit 0, then six letters or digits. */
export function isIfsc(value) {
return false;
}
/** True when the whole value is a six-digit PIN code whose first digit is from 1 to 9. */
export function isPin(value) {
return false;
}
/** True when the whole value is a day slash month slash four-digit year, and the three numbers name a real day. */
export function isDate(value) {
return false;
}
/** True when the whole value is four decimal octets separated by dots, each from 0 to 255 with no leading zeros. */
export function isIpv4(value) {
return false;
} The sample tests · validators.test.mjs
// Sample tests for the id-and-date-validators exercise.
//
// Test names are shown to the learner, so no full dates are written here; the dates live in the data lists below.
import { test, assert } from 'mysmartcopilot:test';
import { PATTERNS, isIfsc, isPin, isDate, isIpv4 } from './validators.mjs';
const VALID_DATES = ['01/01/2024', '29/02/2024', '28/02/2023', '31/12/2024', '30/04/2023'];
const WRONG_CALENDAR = ['29/02/2023', '31/02/2026', '31/04/2026', '15/13/2026', '32/01/2026'];
const WRONG_SHAPES = ['1/1/2024', '01-01-2024', '01/01/24', '2024/01/01', 'aa/bb/cccc', '01/01/2024\n', ''];
test('isIfsc accepts a real 11-character code', () => {
assert.equal(isIfsc('SBIN0000691'), true);
});
test('isIfsc rejects near misses: lower case, wrong length, trailing space', () => {
for (const s of ['sbin0000691', 'SBIN00006911', 'SBIN0000691 ', 'SBIN0000691\n', 'SBINA000691', 'SBINO000691', '']) {
assert.equal(isIfsc(s), false, JSON.stringify(s));
}
});
test('isPin accepts a six-digit code with a non-zero first digit', () => {
for (const s of ['400001', '110001', '744101', '800001']) {
assert.equal(isPin(s), true, JSON.stringify(s));
}
});
test('isPin rejects a leading zero, five digits, seven digits and a stray newline', () => {
for (const s of ['011234', '40000', '7441012', '400001\n', ' 400001', '', '40000a']) {
assert.equal(isPin(s), false, JSON.stringify(s));
}
});
test('isDate accepts a day that exists in the given month and year', () => {
for (const s of VALID_DATES) {
assert.equal(isDate(s), true, JSON.stringify(s));
}
});
test('isDate rejects a day the calendar refuses, such as the leap day outside a leap year', () => {
for (const s of WRONG_CALENDAR) {
assert.equal(isDate(s), false, JSON.stringify(s));
}
});
test('isDate rejects shapes that are not day slash month slash four-digit year', () => {
for (const s of WRONG_SHAPES) {
assert.equal(isDate(s), false, JSON.stringify(s));
}
});
test('isIpv4 accepts a valid IPv4 address', () => {
for (const s of ['0.0.0.0', '127.0.0.1', '192.168.1.1', '255.255.255.255', '10.0.1.42']) {
assert.equal(isIpv4(s), true, JSON.stringify(s));
}
});
test('isIpv4 rejects an octet above 255, a leading zero, three octets and a stray space', () => {
for (const s of ['256.0.0.1', '192.168.01.1', '1.2.3', '1.2.3.4.5', ' 192.168.1.1', '192.168.1.1\n', '01.2.3.4']) {
assert.equal(isIpv4(s), false, JSON.stringify(s));
}
});
test('PATTERNS keeps a non-empty pattern for each format, written with [0-9] rather than the shorthand', () => {
for (const key of ['ifsc', 'pin', 'date', 'ipv4']) {
assert.ok(key in PATTERNS, `PATTERNS is missing ${key}`);
const source = PATTERNS[key].source;
assert.ok(source && source !== '(?:)', `PATTERNS.${key} is empty`);
if (key === 'pin' || key === 'date' || key === 'ipv4') {
const noStrings = source.replace(/\\\\|\\./g, '_');
assert.ok(!noStrings.includes('\\d'), `PATTERNS.${key} uses \\d; write [0-9] instead`);
}
}
}); Python · Starter code · validators.py
"""validators.py: four validators that cover the whole input and reject near misses.
Keep each shape pattern in PATTERNS: the tests read it. The real checks (the calendar for a date, the parser for
an IPv4 address) run after the shape passes.
"""
import re
# Starter patterns: fill in the four with the lesson's rules.
PATTERNS = {
"ifsc": re.compile(r""),
"pin": re.compile(r""),
"date": re.compile(r""),
"ipv4": re.compile(r""),
}
def is_ifsc(value: str) -> bool:
"""True when the whole value is an 11-character IFSC: four letters, the digit 0, then six letters or digits."""
return False
def is_pin(value: str) -> bool:
"""True when the whole value is a six-digit PIN code whose first digit is from 1 to 9."""
return False
def is_date(value: str) -> bool:
"""True when the whole value is a day slash month slash four-digit year, and the three numbers name a real day."""
return False
def is_ipv4(value: str) -> bool:
"""True when the whole value is four decimal octets separated by dots, each from 0 to 255 with no leading zeros."""
return False The sample tests · test_validators.py
"""Sample tests for the id-and-date-validators exercise.
Test names are shown to the learner, so no full dates are written here; the dates live in the data lists below.
"""
import re
from validators import PATTERNS, is_date, is_ifsc, is_ipv4, is_pin
def test_ifsc_accepts_a_real_code():
"""is_ifsc accepts a real 11-character code"""
assert is_ifsc("SBIN0000691") is True
def test_ifsc_rejects_near_misses():
"""is_ifsc rejects near misses: lower case, wrong length, trailing space"""
for s in ["sbin0000691", "SBIN00006911", "SBIN0000691 ", "SBIN0000691\n", "SBINA000691", "SBINO000691", ""]:
assert is_ifsc(s) is False, repr(s)
def test_pin_accepts_a_valid_code():
"""is_pin accepts a six-digit code with a non-zero first digit"""
for s in ["400001", "110001", "744101", "800001"]:
assert is_pin(s) is True, repr(s)
def test_pin_rejects_wrong_shapes():
"""is_pin rejects a leading zero, five digits, seven digits and a stray newline"""
for s in ["011234", "40000", "7441012", "400001\n", " 400001", "", "40000a"]:
assert is_pin(s) is False, repr(s)
# Dates for the test cases below. The lists live here as data; the test names above do not spell them out.
VALID_DATES = ["01/01/2024", "29/02/2024", "28/02/2023", "31/12/2024", "30/04/2023"]
WRONG_CALENDAR = ["29/02/2023", "31/02/2026", "31/04/2026", "15/13/2026", "32/01/2026"]
WRONG_SHAPES = ["1/1/2024", "01-01-2024", "01/01/24", "2024/01/01", "aa/bb/cccc", "01/01/2024\n", ""]
def test_date_accepts_a_real_day():
"""is_date accepts a day that exists in the given month and year"""
for s in VALID_DATES:
assert is_date(s) is True, repr(s)
def test_date_rejects_a_day_the_calendar_refuses():
"""is_date rejects a day the calendar refuses, such as the leap day outside a leap year"""
for s in WRONG_CALENDAR:
assert is_date(s) is False, repr(s)
def test_date_rejects_wrong_shapes():
"""is_date rejects shapes that are not day slash month slash four-digit year"""
for s in WRONG_SHAPES:
assert is_date(s) is False, repr(s)
def test_ipv4_accepts_a_valid_address():
"""is_ipv4 accepts a valid IPv4 address"""
for s in ["0.0.0.0", "127.0.0.1", "192.168.1.1", "255.255.255.255", "10.0.1.42"]:
assert is_ipv4(s) is True, repr(s)
def test_ipv4_rejects_near_misses():
"""is_ipv4 rejects an octet above 255, a leading zero, three octets and a stray space"""
for s in ["256.0.0.1", "192.168.01.1", "1.2.3", "1.2.3.4.5", " 192.168.1.1", "192.168.1.1\n", "01.2.3.4"]:
assert is_ipv4(s) is False, repr(s)
def test_patterns_hold_the_rules():
"""PATTERNS keeps a non-empty pattern for each format, written with [0-9] rather than the shorthand d"""
for key in ["ifsc", "pin", "date", "ipv4"]:
assert key in PATTERNS, f"PATTERNS is missing {key!r}"
source = PATTERNS[key].pattern
assert source, f"PATTERNS[{key!r}] is empty"
if key in ("pin", "date", "ipv4"):
no_strings = re.sub(r"\\\\|\\.", "_", source) # remove escaped characters
assert "\\d" not in no_strings, f"PATTERNS[{key!r}] uses \\d; write [0-9] instead" A hint
Start with the shape patterns in PATTERNS. The IFSC and the PIN code come from the lesson almost unchanged. For the date and the IPv4 address, write the shape with [0-9] groups and let the second layer do the real check (datetime.date or ipaddress.ip_address in Python; a leap-year function and an octet range check in JavaScript). Wrap each pattern with the whole-input anchors your engine uses: \A and \z with re.fullmatch in Python, and ^ and $ without the m flag in JavaScript.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (QuickJS, Pyodide): nothing is sent to mysmartcopilot.com. The first run of each language downloads it: JavaScript (about 0.6 MB) or Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Then the self-check covers all six sections, starting with the one your answers show least:
Practice that adapts to you
Practise this lesson
16 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Warm-up (fresher to 2-5 years): why must a validator match the whole input, and how do the anchors differ by engine?
A search succeeds when the pattern matches anywhere, so re.search(r"[A-Z]{4}0[A-Z0-9]{6}", "foo SBIN0000691 bar")
returns a match object and the stray text passes. A validator has to cover every character, so Python uses
re.fullmatch, JavaScript wraps the pattern with ^ and $ and runs without the m flag, and Go uses \A and
\z. The dollar sign is the piece that varies: in Python, $ without the m flag lets a trailing newline through
and \z does not; in JavaScript, $ holds only at the end of the string without m; in Go, $ and \z behave the
same way at the end. Pick the pair of anchors that forbid a stray newline in the engine you ship, and name them in a
test.
Warm-up (fresher to 2-5 years): why is [0-9] safer than \d for an ID?
Each engine fixes what \d holds. In Python’s re with the default Unicode flag, \d matches every code point of
general category Nd, so a Devanagari ७ passes \d+. ECMAScript’s \d is only the ten ASCII digits, and Go’s regexp
follows RE2 and does the same. A validator for a code that is specified as digits 0 to 9, such as a PIN code or an
IFSC’s branch part, should write [0-9]: it reads the same in every engine, no flag changes it, and the Devanagari
digit the Python pattern silently accepts no longer slips through. Keep \d for text in a language where other
scripts’ digits really do count, and add the (?a:...) inline flag in Python when you want its ASCII meaning for a
section of the pattern.
Two situations put the five layers to work on a design question. Before you look at the first step of either, decide which layer you would build first and why.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A bank-details form takes an IFSC, a MICR code, an account number, a UK sort code (six digits) and a SWIFT code (eight or eleven characters). Each format comes from a different issuer. Design the validation layer: what runs where, how do you report an error that a user can act on, and what is the test plan?
-
Mid to senior
A warehouse system accepts a product barcode and a batch date. A loss report shows that a scanner has sent invalid barcodes with the right shape and dates that no calendar has: the validator passed strings its downstream rejected. What was missing, and how do you fix it without changing the shape pattern?
Flashcards · 7 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
Why did ^[A-Z]{4}0[A-Z0-9]{6}$ under Python's re let a 12-character IFSC into the database?
In Python $ also holds before a final newline, so eleven valid characters plus a newline matched. Use re.fullmatch or \A and \z, and cap the length before the pattern runs.
-
Why is re.search the wrong call for a validator, even with a correct pattern?
search is satisfied by a match anywhere, so foo SBIN0000691 bar passes. re.fullmatch has to cover every character of the value.
-
When should a numeric range move out of the pattern and into code?
A short range such as 0 to 255 still fits in an alternation of four branches. For a wide one such as 1 to 65535, keep the shape as the regex and compare the number in code.
-
What do the official definitions of an IFSC and a PIN code give a pattern to follow?
An IFSC is four letters, the digit zero and six letters or digits; a PIN code is a zone digit followed by five more digits. A shape check still cannot know whether that branch or PIN code exists.
-
Why is a check digit such as Luhn's computed in code rather than in the pattern?
It catches a single mistyped digit and most swaps of neighbouring digits, which every shape accepts, and it is a short loop run after the shape passes. A pattern for it could exist in principle, but nobody could read it closely enough to trust it.
-
Why does ^\d{6}$ accept १२३४५६ in Python, and what should an ID pattern use?
Python's \d matches decimal digits of every script by default, while JavaScript's and Go's are ASCII only. Write [0-9]{6}, which reads the same in every engine and under every flag, or (?a:\d{6}) for Python's ASCII meaning.
-
How do you validate an IPv4 address or a calendar date in Python?
Check the shape with a pattern, then let the ipaddress or datetime module decide whether the value really exists.
Key takeaways
- A validator covers the whole input, so the Python call is
re.fullmatch, in JavaScript the pattern is wrapped with^and$without the m flag, and in Go the anchors are\Aand\z;$alone lets a trailing newline through in Python, and a length cap, which OWASP’s rule for string fields starts with, comes before the pattern. - A pattern is right for a shape rule, such as an optional sign, no leading zeros and two decimals; a numeric range like 0 to 255 fits an alternation, and a wider range belongs in code.
- IFSC and PIN codes come from an official definition the pattern can read line by line: four letters, the digit zero and six characters for IFSC, and a zone digit followed by five more digits for a PIN code.
- A check digit, such as a Luhn total that ends in zero, is a short loop that runs after the shape passes: a pattern for it could exist in principle, but nobody could read it closely enough to trust it.
[0-9]is the safest spelling for a digit of an ID, since Python’s\dmatches digits of other scripts and JavaScript’s and Go’s do not, and no flag changes what[0-9]holds.- An IPv4 address and a dd slash mm slash yyyy date are the same recipe: the shape as a pattern, and
ipaddressordatetimeto decide whether the value exists.
Terms in this lesson
- allowlist validation
- A rule that says what a field accepts (length, allowed characters, shape) and refuses everything else, instead of listing values to block. OWASP's Input Validation Cheat Sheet treats it as the default.
- check digit
- A digit at the end of a number computed from the digits before it, used to catch a single wrong digit and most swaps of two neighbouring digits when the number is read or typed by hand. A short loop computes it after a pattern has checked the shape.
- Luhn algorithm
- A check-digit rule from ISO/IEC 7812-1 Annex B. Double every second digit from the right, add the digits of each doubled result to the rest, and the total must end in zero. Used for payment card numbers and IMEIs.
- shape check
- A pattern that decides whether a string is written in a given form, such as four letters, a 0 and six characters. It says nothing about whether the value exists; the existence check runs after it.
References
- FAQs on National Electronic Funds Transfer (NEFT): the 11-character Indian Financial System Code (Reserve Bank of India)
- PIN Code: six digits whose first digit is the postal zone (Department of Posts (Tamil Nadu Postal Circle, as mirrored by the Internet Archive))
- Input Validation Cheat Sheet: allowlist validation and length limits (OWASP)
- re: fullmatch, the \A and \Z anchors, and ASCII and Unicode flags (Python Software Foundation)
- ipaddress: strict IPv4 parsing refuses leading zeros since Python 3.9.5 (Python Software Foundation)
- ECMAScript 2026 Language Specification (ECMA-262): CharacterClassEscape \d (Ecma International)
- regexp: Go's regexp package, MatchString and the RE2 syntax (The Go Authors)
- RE2 syntax: the anchors and flags Go's regexp reads (Google)
- ISO/IEC 7812-1:2017, the numbering system of card issuers, with the Luhn check digit in Annex B (ISO and IEC)
- HTML Standard: a valid email address, the rule the input element applies (WHATWG)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress