Skip to content

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+$ accepts a@b@c — \S includes @. 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} accepts 999. 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\d accepts 2026-13-45. The regex finds the shape; the date library rejects the value. Same division of labour.
  • Rows 5–6, decimals. \d+\.\d+ on v1.2.3 finds 1.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