Common patterns and their traps¶
One line: The six patterns everybody writes from memory — email, IPv4, date, decimal, quoted string, signed number — each accept an input they should not, and every engine here agrees about it, because the fault is in the pattern's shape, not in any engine; the fix is usually to stop asking a regex to check semantics it cannot see.
Level: 201 · you were about to paste one of these
The rows¶
A regex checks the shape of text. It cannot check that a month is at most 12, that an octet is at most 255, or that a domain exists. Every trap below is a shape that is right and a value that is wrong.
Measured¶
Verified output of topic_common_patterns_and_their_traps_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
construct python perl node ruby java go rust fancy
email: \S+@\S+ takes two @ a@b@c a@b@c a@b@c a@b@c a@b@c a@b@c a@b@c a@b@c
email: one @ enforced no no no no no no no no
IPv4: \d{1,3} allows 999 999.999.999.999 999.999.999.999 999.999.999.999 999.999.999.999 999.999.999.999 999.999.999.999 999.999.999.999 999.999.999.999
date: shape is not validity 2026-13-45 2026-13-45 2026-13-45 2026-13-45 2026-13-45 2026-13-45 2026-13-45 2026-13-45
decimal: \d+\.\d+ on 1.2.3 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2
decimal: \d+(\.\d+)? first 1.2 1.2 1.2 1.2 1.2 1.2 1.2 1.2
quoted: "(.*)" is greedy "a" and "b" "a" and "b" "a" and "b" "a" and "b" "a" and "b" "a" and "b" "a" and "b" "a" and "b"
quoted: "([^"]*)" is not "a" "a" "a" "a" "a" "a" "a" "a"
number: -?\d+ on - no no no no no no no no
number: .5 without a zero no no no no no no no no
Reading the table¶
- Rows 1–2, email.
^\S+@\S+$acceptsa@b@c—\Sincludes@. Excluding it,^[^@\s]+@[^@\s]+$, rejects the double. That is about as far as a regex should go with an email address: one@, no spaces, something on each side. The RFC 5322 grammar is not a regular language and the famous 6,000-character pattern that approximates it rejects valid addresses; validate by sending mail. - Row 3, IPv4.
\d{1,3}accepts999. The pattern that does not —(25[0-5]|2[0-4]\d|1?\d?\d)four times — is correct and unreadable. Every language here has an address parser (ipaddress,inet_pton,net.ParseIP,Ipv4Addr::from_str); use it and let the regex find candidates. - Row 4, dates.
\d{4}-\d\d-\d\daccepts2026-13-45. The regex finds the shape; the date library rejects the value. Same division of labour. - Rows 5–6, decimals.
\d+\.\d+onv1.2.3finds1.2, and so does\d+(\.\d+)?— the version string has two dots and the pattern stops at the first. Neither answer is wrong; both are a pattern being asked a question it was not designed for. Anchor, or decide what a version number is. - Rows 7–8, quoted strings.
"(.*)"on"a" and "b"takes everything from the first quote to the last — greedy."([^"]*)"takes"a". The class is the fix, not a lazy quantifier:"(.*?)"also gives"a"here but still cannot handle an escaped quote, and is slower on long subjects. - Rows 9–10, numbers.
^-?\d+$rejects a lone-, correctly.^\d+\.\d+$rejects.5, which may or may not be what you wanted — and\d*\.?\d*accepts the empty string, which never is. Decide the grammar first, then write it.
The pattern behind the patterns¶
Each of these has the same shape: the regex is asked to do the whole job, and the job has a part that is not about shape. The reliable split is regex finds the candidate, code validates the value. It reads better, the pattern gets shorter, and the error message can say why — "month 13" instead of "does not match".
Keywords on this page¶
^ · $ · Shorthand classes · Character class · {n,m} · Lazy quantifier · Alternation.
See also¶
- Validating input — the anchoring half of the same problem.
- Catastrophic backtracking — the email validator that never answers.
- What
\wand\bmatch — why\din an IPv4 pattern is already not ASCII in three engines.