← All stacks
RX

Regular Expressions

11 sections · 86 entries

Basic Patterns

Literal Match

syntax
/hello/  or  r'hello'
example
JS:  'say hello world'.match(/hello/)
Py:  re.search(r'hello', 'say hello world')
output
Matches "hello" at index 4

Note Characters that are not special match themselves exactly. Regex is case-sensitive by default -- use the i flag for case-insensitive matching.

Dot (Any Character)

syntax
.  (matches any single character except newline)
example
JS:  'bat bet bit'.match(/b.t/g)
Py:  re.findall(r'b.t', 'bat bet bit')
output
["bat", "bet", "bit"]

Note The dot does NOT match newline (\n) by default. Use the s (dotall) flag to make dot match newline as well. A common beginner mistake is using . when you mean a literal period -- escape it as \. instead.

^ Start of String

syntax
^pattern
example
JS:  /^Error/.test('Error: file missing')   // true
JS:  /^Error/.test('An Error occurred')   // false
Py:  bool(re.match(r'^Error', 'Error: file missing'))  # True
output
true, false, True

Note Matches position at start of string. With the m (multiline) flag, ^ matches the start of each line instead. Do not confuse this with [^...] inside a character class, which means negation.

$ End of String

syntax
pattern$
example
JS:  /\.json$/.test('config.json')   // true
JS:  /\.json$/.test('config.json.bak')  // false
Py:  bool(re.search(r'\.json$', 'config.json'))  # True
output
true, false, True

Note Matches position at end of string. With the m flag, $ matches the end of each line. In Python, re.match only checks the start -- use re.search with $ to check the end.

\b Word Boundary

syntax
\bword\b
example
JS:  'catfish is not a cat'.match(/\bcat\b/g)
Py:  re.findall(r'\bcat\b', 'catfish is not a cat')
output
["cat"]  (matches only the standalone word, not "catfish")

Note A word boundary is the position between a word character (\w) and a non-word character. Essential for whole-word searches. In JS, remember to double-escape in strings: new RegExp('\\bcat\\b').

\B Non-Word Boundary

syntax
\Bpattern  or  pattern\B
example
JS:  'catfish is not a cat'.match(/\Bcat/g)
Py:  re.findall(r'\Bcat', 'catfish is not a cat')
output
[]  (no match -- 'cat' always starts at a word boundary here)

'scattered'.match(/\Bcat\B/g) => ["cat"]  (inside a word)

Note \B matches every position that is NOT a word boundary -- useful for finding substrings embedded within larger words.

Escaping Special Characters

syntax
\. \* \+ \? \( \) \[ \] \{ \} \^ \$ \| \\
example
JS:  'Price: $9.99'.match(/\$\d+\.\d{2}/)
Py:  re.search(r'\$\d+\.\d{2}', 'Price: $9.99')
output
"$9.99"

Note All special regex characters must be escaped with a backslash to match literally. The special characters are: . * + ? ( ) [ ] { } ^ $ | \. In Python raw strings (r'...'), you only escape for regex, not for Python itself.

Character Classes

Basic Character Class [abc]

syntax
[abc]  matches a single a, b, or c
example
JS:  'gray vs grey'.match(/gr[ae]y/g)
Py:  re.findall(r'gr[ae]y', 'gray vs grey')
output
["gray", "grey"]

Note A character class matches exactly one character from the set. Most metacharacters lose their special meaning inside brackets -- for example, [.+*] matches a literal dot, plus, or asterisk.

Ranges [a-z] [A-Z] [0-9]

syntax
[a-z]  [A-Z]  [0-9]  [a-zA-Z0-9]
example
JS:  'Room 4B'.match(/[A-Z][0-9]/)
Py:  re.search(r'[0-9][A-Z]', 'Room 4B')
output
JS: null (no uppercase letter followed by digit)
Py: "4B" (digit followed by uppercase letter)

Note Order matters. [a-z] covers lowercase Latin letters. Combine ranges for broader matches. Ranges use Unicode/ASCII code points, so [A-z] accidentally includes [, \, ], ^, _, ` -- always use [A-Za-z] instead.

Negated Class [^abc]

syntax
[^abc]  matches any character EXCEPT a, b, or c
example
JS:  'a1b2c3'.match(/[^a-z]/g)
Py:  re.findall(r'[^a-z]', 'a1b2c3')
output
["1", "2", "3"]

Note The caret ^ must be the first character inside the brackets to negate. Placed anywhere else, it matches a literal ^. Common gotcha: [^abc] still matches newlines unless excluded.

\d Digit and \D Non-Digit

syntax
\d  =>  [0-9]    \D  =>  [^0-9]
example
JS:  'Invoice #2084'.match(/\d+/)
Py:  re.findall(r'\D+', 'abc-123-xyz')
output
JS: "2084"
Py: ["abc-", "-xyz"]

Note In Python 3 with the default engine, \d can match Unicode digits (e.g., Arabic-Indic digits) unless you use re.ASCII. In JavaScript, \d always means [0-9].

\w Word Character and \W

syntax
\w  =>  [a-zA-Z0-9_]    \W  =>  [^a-zA-Z0-9_]
example
JS:  'user_name@host'.match(/\w+/g)
Py:  re.findall(r'\W+', 'hello, world! 42')
output
JS: ["user_name", "host"]
Py: [", ", "! "]

Note \w includes the underscore. Like \d, Python's \w matches Unicode letters by default (accented chars, CJK, etc.) while JS \w sticks to ASCII unless the u flag is combined with Unicode property escapes.

\s Whitespace and \S

syntax
\s  =>  [ \t\n\r\f\v]    \S  =>  [^ \t\n\r\f\v]
example
JS:  'hello   world'.split(/\s+/)
Py:  re.split(r'\s+', '  line one\n  line two  ')
output
JS: ["hello", "world"]
Py: ["", "line", "one", "line", "two", ""]

Note \s matches spaces, tabs, newlines, carriage returns, form feeds, and vertical tabs. In Python, re.split on a leading/trailing whitespace produces empty strings at the edges -- use str.split() if you want to avoid them.

Dot Inside Character Class

syntax
[.]  matches a literal period (no escaping needed)
example
JS:  'v2.1.0'.match(/[.]/g)
Py:  re.findall(r'[.]', '192.168.0.1')
output
JS: [".", "."]
Py: [".", ".", "."]

Note Inside a character class, the dot loses its special meaning and matches a literal period. Both [.] and \. work for matching periods, but [.] can be more readable.

Custom Ranges & Combinations

syntax
[a-fA-F0-9]  [\w.-]  [\s\S]
example
JS:  '#ff8C2a'.match(/^#[a-fA-F0-9]{6}$/)
Py:  re.findall(r'[\w.-]+', 'file-name_v2.txt')
output
JS: ["#ff8C2a"]  (valid hex color)
Py: ["file-name_v2.txt"]

Note [\s\S] is a classic trick to match ANY character including newlines (before the s flag existed). Place a hyphen at the start or end of a class [-abc] or [abc-] to match it literally, or escape it [a\-z].

Quantifiers

* Zero or More

syntax
a*  matches zero or more 'a' characters
example
JS:  'caaandy'.match(/ca*/)
Py:  re.findall(r'bo*', 'A bird booed and a bobcat roared')
output
JS: "caaa"
Py: ["boo", "bo", "b"]

Note * is greedy by default -- it matches as many characters as possible. Beware: .* can match empty strings and lead to unexpected results at every position in a string.

+ One or More

syntax
a+  matches one or more 'a' characters
example
JS:  'caaandy candy cdy'.match(/ca+ndy/g)
Py:  re.findall(r'\d+', 'There are 12 cats and 340 dogs')
output
JS: ["caaandy", "candy"]
Py: ["12", "340"]

Note Unlike *, the + quantifier requires at least one occurrence. This is the most common quantifier for extracting sequences of digits, letters, etc.

? Optional (Zero or One)

syntax
a?  matches zero or one 'a'
example
JS:  'color colour'.match(/colou?r/g)
Py:  re.findall(r'https?://', 'http://a.com and https://b.com')
output
JS: ["color", "colour"]
Py: ["http://", "https://"]

Note Makes the preceding element optional. Extremely useful for handling spelling variations and optional protocol prefixes.

{n} Exact Count

syntax
a{3}  matches exactly 3 'a' characters
example
JS:  '1234-5678-9012-3456'.match(/\d{4}/g)
Py:  re.findall(r'\b[A-Z]{2}\b', 'Go to NY or LA, not NYC')
output
JS: ["1234", "5678", "9012", "3456"]
Py: ["NY", "LA"]

Note Matches the preceding element exactly n times. Useful for fixed-width fields like credit card groups, zip codes, and two-letter state codes.

{n,m} Range Count

syntax
a{2,4}  matches 2 to 4 'a' characters
example
JS:  'aaa aa aaaa a aaaaa'.match(/\ba{2,4}\b/g)
Py:  re.findall(r'\b\w{3,5}\b', 'I am a regex pro now')
output
JS: ["aaa", "aa", "aaaa"]
Py: ["regex", "pro", "now"]

Note Greedy by default -- matches the maximum count first, then backtracks. {n,} means n or more with no upper limit. {0,1} is equivalent to ?.

{n,} Minimum Count

syntax
a{3,}  matches 3 or more 'a' characters
example
JS:  'password1234'.match(/.{8,}/)
Py:  re.search(r'\d{3,}', 'My code is 42 or maybe 12345')
output
JS: "password1234"  (8+ chars)
Py: "12345"  (3+ digits)

Note Useful for minimum-length validation. Remember this is greedy -- it will consume as much as possible.

Greedy vs Lazy Matching

syntax
Greedy: .*  .+  .?     Lazy: .*?  .+?  .??
example
JS:  '<b>bold</b> and <b>more</b>'.match(/<b>.*<\/b>/)
JS:  '<b>bold</b> and <b>more</b>'.match(/<b>.*?<\/b>/)
output
Greedy: "<b>bold</b> and <b>more</b>"  (longest match)
Lazy:   "<b>bold</b>"  (shortest match)

Note Greedy quantifiers eat as much as possible, then backtrack. Lazy quantifiers match as little as possible, then expand. This distinction is critical when parsing HTML, quoted strings, or any delimited content. Always prefer lazy when you want the nearest closing delimiter.

Lazy Quantifier Variants

syntax
*?  +?  ??  {n,m}?  {n,}?
example
JS:  '"hello" and "world"'.match(/"[^"]+?"/g)
Py:  re.findall(r'\d{2,4}?', '12345')
output
JS: ['"hello"', '"world"']
Py: ["12", "34"]  (minimum 2 digits each time)

Note Append ? after any quantifier to make it lazy. {2,4}? prefers matching 2 characters instead of 4. For the quoted-string case, a negated class [^"]+ is often more efficient than a lazy .+? approach.

Possessive Quantifiers (Concept)

syntax
*+  ++  ?+  {n,m}+   (not supported in JS or Python re)
example
Java/PCRE:  '"hello"'.match(/"[^"]*+"/)
Python alternative: use atomic group via regex module
  import regex
  regex.search(r'"(?>"[^"]*)"+', text)
output
Matches "hello" without backtracking into the quoted content

Note Possessive quantifiers never give back characters once matched. They prevent catastrophic backtracking but cause a match to fail if the rest of the pattern cannot match. Not available in JavaScript or Python's built-in re module -- use the third-party 'regex' module in Python or atomic groups in PCRE.

Groups & Alternation

Capturing Group ()

syntax
(pattern)  captures the matched text
example
JS:  'Date: 2026-04-04'.match(/(\d{4})-(\d{2})-(\d{2})/)
Py:  m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2026-04-04')
     m.group(1), m.group(2), m.group(3)
output
JS: ["2026-04-04", "2026", "04", "04"]
Py: ('2026', '04', '04')

Note Each pair of parentheses creates a numbered capture group starting at 1. In JS, the full match is at index 0. Groups are essential for extracting parts of a match for later use.

Non-Capturing Group (?:)

syntax
(?:pattern)  groups without capturing
example
JS:  'http://a.com https://b.com'.match(/(?:https?):\/\/\S+/g)
Py:  re.findall(r'(?:Mr|Mrs|Ms)\.\s(\w+)', 'Mr. Smith and Ms. Lee')
output
JS: ["http://a.com", "https://b.com"]
Py: ["Smith", "Lee"]  (only the name is captured)

Note Use (?:) when you need grouping for alternation or quantifiers but do not need to capture the result. Reduces memory usage and keeps group numbering clean.

Named Capturing Groups

syntax
JS: (?<name>pattern)    Python: (?P<name>pattern)
example
JS:  const m = '2026-04-04'.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/)
     m.groups.year  // "2026"
Py:  m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', '2026-04-04')
     m.group('year')  # '2026'
output
JS: m.groups => { year: "2026", month: "04", day: "04" }
Py: m.group('year') => '2026'

Note Syntax differs between JS and Python. JS uses (?<name>...) while Python uses (?P<name>...). Named groups dramatically improve readability of complex patterns. Both approaches also assign a numeric index.

Backreferences \1

syntax
\1  \2  etc. refer to previously captured groups
example
JS:  'abcabc'.match(/(abc)\1/)
Py:  re.search(r'(\w+)\s+\1', 'the the quick brown fox')
output
JS: ["abcabc", "abc"]
Py: matches "the the"  (detects duplicate word)

Note The backreference matches the exact same text captured by group N, not the same pattern. Useful for detecting repeated words or matched delimiters (like matching opening and closing quotes of the same type).

Named Backreferences

syntax
JS: \k<name>    Python: (?P=name)
example
JS:  /(?<quote>['"]).*?\k<quote>/.exec(`She said "hello" and 'bye'`)
Py:  re.search(r"(?P<quote>['\"]).*?(?P=quote)", 'He said "ok"')
output
JS: ['"hello"', '"']
Py: '"ok"'

Note Named backreferences are more readable than \1 in complex patterns. Syntax differs: JS uses \k<name>, Python uses (?P=name).

Alternation |

syntax
pattern1|pattern2
example
JS:  'My cat and your dog'.match(/cat|dog/g)
Py:  re.findall(r'\b(?:jpg|png|gif)\b', 'files: logo.png and bg.jpg')
output
JS: ["cat", "dog"]
Py: ["png", "jpg"]

Note The | operator has very low precedence -- /ab|cd/ matches 'ab' or 'cd', NOT 'a(b|c)d'. Always wrap alternations in a group when they are part of a larger pattern to avoid surprises.

Nested Groups

syntax
((inner) outer)  groups inside groups
example
JS:  '192.168.1.1'.match(/((\d{1,3})\.){3}(\d{1,3})/)
Py:  m = re.search(r'((\d{1,3})\.){3}(\d{1,3})', '192.168.1.1')
     m.group(0)  # '192.168.1.1'
output
Full match: "192.168.1.1"
Group 1: "1." (last repetition of the outer group)
Group 2: "1" (last repetition of the inner group)
Group 3: "1"

Note When a group is repeated with a quantifier, only the LAST capture is retained. If you need all repetitions, use findall or matchAll instead. Numbering is left-to-right by opening parenthesis.

Lookahead & Lookbehind

Positive Lookahead (?=)

syntax
pattern(?=ahead)  matches pattern only if followed by ahead
example
JS:  '100px 200em 50px'.match(/\d+(?=px)/g)
Py:  re.findall(r'\d+(?=px)', '100px 200em 50px')
output
["100", "50"]  (numbers followed by 'px', but 'px' is not in the match)

Note Lookaheads are zero-width assertions -- they check what comes next without consuming characters. The looked-ahead text is not part of the match result. This means subsequent patterns can still match those characters.

Negative Lookahead (?!)

syntax
pattern(?!ahead)  matches pattern only if NOT followed by ahead
example
JS:  '3 dogs 4 cats 5 dogs'.match(/\d+(?! cats)/g)
Py:  re.findall(r'\b\w+\.(?!exe\b)\w+', 'run.exe data.csv img.png')
output
JS: ["3", "5"]  (numbers NOT followed by ' cats')
Py: ["data.csv", "img.png"]  (files that are NOT .exe)

Note Negative lookahead is excellent for excluding specific patterns. Watch for off-by-one issues -- ensure the lookahead checks the right position. Commonly used to exclude file types, keywords, or invalid suffixes.

Positive Lookbehind (?<=)

syntax
(?<=behind)pattern  matches pattern only if preceded by behind
example
JS:  '$100 and EUR200'.match(/(?<=\$)\d+/g)
Py:  re.findall(r'(?<=@)\w+', 'user@gmail and admin@corp')
output
JS: ["100"]  (digits preceded by $)
Py: ["gmail", "corp"]  (words preceded by @)

Note Lookbehinds check what comes BEFORE the match position without including it. In JavaScript, lookbehinds were added in ES2018 and are supported in all modern browsers. Python's re module requires fixed-width lookbehinds -- use the third-party regex module for variable-width.

Negative Lookbehind (?<!)

syntax
(?<!behind)pattern  matches pattern only if NOT preceded by behind
example
JS:  'USD 100 and $200'.match(/(?<!\$)\b\d+/g)
Py:  re.findall(r'(?<!\\)\n', 'line1\nline2\\\nline3')
output
JS: ["100"]  (digits NOT preceded by $)

Note Useful for avoiding matches that follow a specific prefix. A classic use case is matching newlines that are not escaped (not preceded by a backslash).

Combining Lookaheads & Lookbehinds

syntax
(?<=prefix)pattern(?=suffix)
example
JS:  'price: [42] and size: [XL]'.match(/(?<=\[)\d+(?=\])/g)
Py:  re.findall(r'(?<=\[)\w+(?=\])', '[hello] and [world]')
output
JS: ["42"]  (digits inside square brackets, without brackets)
Py: ["hello", "world"]

Note You can combine multiple lookarounds on the same position. A common pattern for password validation stacks multiple lookaheads: (?=.*[A-Z])(?=.*\d)(?=.*[@#$]).{8,} checks for uppercase, digit, and special character all in one pass.

Stacked Lookaheads for Validation

syntax
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%]).{8,}$
example
JS:  const strong = /^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%]).{8,}$/
     strong.test('MyP@ss1word')   // true
     strong.test('weakpass')       // false
Py:  pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%]).{8,}$'
     bool(re.match(pattern, 'MyP@ss1word'))  # True
output
true / True for 'MyP@ss1word', false / False for 'weakpass'

Note Each lookahead checks a different requirement without advancing the match position. All must pass before .{8,} consumes the string. This is a widely-used pattern but be cautious about catastrophic backtracking with very long inputs -- consider checking each requirement separately in code for production validation.

Anchors & Boundaries

^ Start Anchor

syntax
^pattern
example
JS:  /^#!/.test('#!/bin/bash')    // true
JS:  /^#!/.test('echo #!/bin')   // false
Py:  bool(re.match(r'^#!', '#!/usr/bin/env python'))  # True
output
true, false, True

Note Without the m flag, ^ matches only the very beginning of the entire string. With the m flag, it matches the start of each line (after every newline character).

$ End Anchor

syntax
pattern$
example
JS:  /\.py$/.test('script.py')       // true
JS:  /\.py$/.test('script.py.bak')   // false
Py:  bool(re.search(r'\.py$', 'main.py'))  # True
output
true, false, True

Note Without the m flag, $ matches the very end of the string (or before a trailing newline in Python). With m, it matches the end of each line.

\b Word Boundary (Detail)

syntax
\b  matches the boundary between \w and \W (or string start/end)
example
JS:  'reinvent invent invention'.match(/\binvent\b/g)
Py:  re.findall(r'\binvent\b', 'reinvent invent invention')
output
["invent"]  (only the standalone word)

Note A word boundary exists at three positions: before the first \w character, after the last \w character, and between a \w and \W character. It does NOT match any characters itself -- it matches a position.

\A Absolute Start of String (Python)

syntax
\A  matches the very start of the string (ignores multiline)
example
Py:  re.search(r'\AFirst', 'First line\nSecond line', re.MULTILINE)
     # Matches 'First'
Py:  re.search(r'^Second', 'First line\nSecond line', re.MULTILINE)
     # Also matches (^ respects multiline)
output
\A always means absolute start, even with re.MULTILINE

Note \A is not available in JavaScript. It is equivalent to ^ when multiline mode is off, but unlike ^, it never changes behavior with the m flag. Use \A in Python when you always mean the beginning of the entire string.

\Z Absolute End of String (Python)

syntax
\Z  matches the very end of the string (ignores multiline)
example
Py:  re.search(r'end\Z', 'start\nmiddle\nend')
     # Matches 'end'
Py:  re.search(r'middle$', 'start\nmiddle\nend', re.MULTILINE)
     # Also matches ($ respects multiline)
output
\Z matches only the absolute end, regardless of flags

Note In Python, \Z matches the absolute end (after the last character). Note: \z (lowercase) is used in some other flavors (Ruby, PCRE) for the same purpose, while Python's \Z (uppercase) does not match before a trailing newline. Not available in JavaScript.

Multiline Behavior with ^/$

syntax
Use m flag to make ^ and $ match line boundaries
example
JS:  'line1\nline2\nline3'.match(/^\w+/gm)
Py:  re.findall(r'^\w+', 'line1\nline2\nline3', re.MULTILINE)
output
["line1", "line2", "line3"]  (each line start matched)

Note Without the m flag, only 'line1' would match ^\w+. A common mistake is forgetting the m flag when processing multi-line text like log files or configuration files. In Python, re.MULTILINE or re.M is the flag; in JS, use /m.

Common Patterns

Email Address (Simple)

syntax
/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/
example
JS:  'contact dev.ops+test@my-company.co.uk now'.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/)
Py:  re.findall(r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', text)
output
"dev.ops+test@my-company.co.uk"

Note This is a practical pattern covering the vast majority of real email addresses. The full RFC 5322 spec is far more complex and almost never needed. For production validation, prefer a library or send a confirmation email.

URL Pattern

syntax
/https?:\/\/[\w.-]+(?:\.[a-zA-Z]{2,})(?:[\/\w._~:?#\[\]@!$&'()*+,;=-]*)/
example
JS:  'Visit https://example.com/path?q=1&lang=en#top today'.match(/https?:\/\/[\w.-]+(?:\.[a-zA-Z]{2,})(?:[\/\w._~:?#@!$&'()*+,;=-]*)/)
Py:  re.findall(r'https?://[\w.-]+(?:\.[a-zA-Z]{2,})(?:[/\w._~:?#@!$&\'()*+,;=-]*)', text)
output
"https://example.com/path?q=1&lang=en#top"

Note URL matching is inherently tricky due to the wide range of valid characters. This pattern handles most common HTTP/HTTPS URLs. For rigorous URL validation, use the URL constructor in JS or urllib.parse in Python.

IPv4 Address

syntax
/\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/
example
JS:  '192.168.1.1 and 999.999.999.999'.match(/\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b/g)
Py:  re.findall(r'\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b', text)
output
["192.168.1.1"]  (999.999.999.999 rejected -- octets must be 0-255)

Note Each octet allows 0-255. The alternation order matters: check 25[0-5] before 2[0-4]\d before the general case. A simpler but less accurate version is \d{1,3}(\.\d{1,3}){3} which does not validate octet ranges.

Phone Number (US Formats)

syntax
/(?:\+?1[-. ]?)?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}/
example
JS:  const phone = /(?:\+?1[-. ]?)?\(?\d{3}\)?[-. ]?\d{3}[-. ]?\d{4}/g
     'Call (555) 123-4567 or +1.800.555.0199'.match(phone)
output
["(555) 123-4567", "+1.800.555.0199"]

Note Phone formats vary wildly by country. This covers common US formats: (555) 123-4567, 555-123-4567, 555.123.4567, +1 555 123 4567. For international numbers, consider a library like libphonenumber.

Date Formats (YYYY-MM-DD / MM/DD/YYYY)

syntax
YYYY-MM-DD: /\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])/
MM/DD/YYYY: /(?:0[1-9]|1[0-2])\/(?:0[1-9]|[12]\d|3[01])\/\d{4}/
example
JS:  '2026-04-04 and 12/25/2025'.match(/\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])/g)
Py:  re.findall(r'(?:0[1-9]|1[0-2])/(?:0[1-9]|[12]\d|3[01])/\d{4}', text)
output
JS: ["2026-04-04"]
Py: ["12/25/2025"]

Note Regex validates format but not logical correctness -- it will accept 02/31/2026 (Feb 31 does not exist). For actual date validation, parse the match with Date (JS) or datetime (Python) afterward.

Username Validation

syntax
/^[a-zA-Z][a-zA-Z0-9._-]{2,19}$/
example
JS:  /^[a-zA-Z][a-zA-Z0-9._-]{2,19}$/.test('dev_user.42')   // true
JS:  /^[a-zA-Z][a-zA-Z0-9._-]{2,19}$/.test('_nope')         // false
JS:  /^[a-zA-Z][a-zA-Z0-9._-]{2,19}$/.test('ab')            // false
output
true, false (starts with _), false (too short: 3-20 chars required)

Note Rules: must start with a letter, 3-20 characters total, allows letters, digits, dots, underscores, and hyphens. Adjust the {2,19} range for your app's requirements (first char + 2-19 more = 3-20 total).

Hex Color Code

syntax
/^#(?:[0-9a-fA-F]{3}){1,2}$/
example
JS:  /^#(?:[0-9a-fA-F]{3}){1,2}$/.test('#ff5733')   // true
JS:  /^#(?:[0-9a-fA-F]{3}){1,2}$/.test('#F3A')      // true
JS:  /^#(?:[0-9a-fA-F]{3}){1,2}$/.test('#xyz')      // false
output
true (6-digit), true (3-digit shorthand), false (invalid hex)

Note Matches both 3-digit (#RGB) and 6-digit (#RRGGBB) hex colors. The {1,2} quantifier on the 3-char group handles both lengths. Does not cover 8-digit RGBA hex -- extend to {1,2,} won't work; add a separate alternation for #RRGGBBAA if needed.

File Extension Extraction

syntax
/\.([a-zA-Z0-9]+)$/
example
JS:  'report-final.v2.pdf'.match(/\.([a-zA-Z0-9]+)$/)[1]
Py:  re.search(r'\.([a-zA-Z0-9]+)$', 'archive.tar.gz').group(1)
output
JS: "pdf"
Py: "gz"

Note Captures only the last extension. For double extensions like .tar.gz, match /\.tar\.gz$/ explicitly or extract both with /\.([a-zA-Z0-9]+(?:\.[a-zA-Z0-9]+)*)$/. Alternatively, use path.extname (Node) or os.path.splitext (Python).

HTML Tag Matching

syntax
/<([a-z][a-z0-9]*)\b[^>]*>(.*?)<\/\1>/i
example
JS:  '<div class="box">content</div>'.match(/<([a-z][a-z0-9]*)\b[^>]*>(.*?)<\/\1>/i)
Py:  re.search(r'<([a-z][a-z0-9]*)\b[^>]*>(.*?)</\1>', html, re.IGNORECASE)
output
Full: "<div class=\"box\">content</div>"
Group 1: "div", Group 2: "content"

Note This works for simple, non-nested cases only. Regex CANNOT reliably parse nested HTML -- nested tags, self-closing tags, comments, and CDATA sections break pattern-based approaches. Use a proper HTML parser (DOMParser, BeautifulSoup, cheerio) for production code.

Whitespace Trimming

syntax
/^\s+|\s+$/g
example
JS:  '  hello world  '.replace(/^\s+|\s+$/g, '')
Py:  re.sub(r'^\s+|\s+$', '', '  hello world  ')
output
"hello world"

Note Equivalent to str.trim() in JS and str.strip() in Python. The regex version is useful when you also want to normalize internal whitespace: replace /\s+/g with ' ' after trimming.

Number with Commas / Decimals

syntax
/^-?\d{1,3}(,\d{3})*(\.\d+)?$/
example
JS:  /^-?\d{1,3}(,\d{3})*(\.\d+)?$/.test('1,234,567.89')  // true
JS:  /^-?\d{1,3}(,\d{3})*(\.\d+)?$/.test('12,34')          // false
output
true, false (incorrect comma placement)

Note Validates US-formatted numbers with optional commas every 3 digits and optional decimal part. European formats swap comma and period -- adjust the pattern accordingly. The leading -? handles negative numbers.

Flags & Modifiers

g - Global Flag

syntax
JS: /pattern/g    Python: (default for findall/sub)
example
JS:  'aaa'.match(/a/g)
JS:  'aaa'.match(/a/)   // without g
Py:  re.findall(r'a', 'aaa')  # global by nature
output
With g: ["a", "a", "a"]
Without g: ["a"] (first match only)
Python findall: ['a', 'a', 'a']

Note In JS, the g flag makes match() return all matches instead of just the first. Without g, match() returns the first match with capture groups. Python's re.findall and re.sub are inherently global. Use re.search for first-match-only behavior in Python.

i - Case-Insensitive Flag

syntax
JS: /pattern/i    Python: re.IGNORECASE or re.I
example
JS:  /hello/i.test('Hello World')   // true
Py:  re.search(r'hello', 'Hello World', re.IGNORECASE)
output
Matches 'Hello' despite different casing

Note Affects all literal characters and character classes in the pattern. [a-z] with the i flag will also match uppercase letters. Be aware that case-folding in Unicode can be surprising -- the German sharp s (ss) may match SS in some engines.

m - Multiline Flag

syntax
JS: /pattern/m    Python: re.MULTILINE or re.M
example
JS:  'apple\nbanana\ncherry'.match(/^\w+/gm)
Py:  re.findall(r'^\w+', 'apple\nbanana\ncherry', re.M)
output
["apple", "banana", "cherry"]

Note Changes ^ and $ from matching string start/end to matching line start/end. Does NOT change the behavior of \A and \Z in Python, nor does it affect how the dot treats newlines. Combine with g in JS for all line-start matches.

s - Dotall / Single-Line Flag

syntax
JS: /pattern/s    Python: re.DOTALL or re.S
example
JS:  'line1\nline2'.match(/line1.line2/)    // null
JS:  'line1\nline2'.match(/line1.line2/s)   // matches
Py:  re.search(r'line1.line2', 'line1\nline2', re.DOTALL)
output
Without s: no match (dot skips \n)
With s: "line1\nline2"

Note The s flag makes the dot (.) match newline characters too. Before this flag existed, the workaround was [\s\S] to match any character including newlines. The s flag was added to JS in ES2018.

u - Unicode Flag

syntax
JS: /pattern/u    Python: (Unicode by default in Python 3)
example
JS:  /^.$/u.test('[U+D83D][U+DE00]')    // true (emoji as one char)
JS:  /^.$/.test('[U+D83D][U+DE00]')     // false (two UTF-16 surrogates)
JS:  /\p{Emoji}/u.test('Hi')    // true (enables \p{} syntax)
output
With u: emoji treated as single character
Without u: emoji seen as two surrogate code units

Note In JS, the u flag enables full Unicode matching: surrogate pairs are treated as single code points, and Unicode property escapes (\p{...}) become available. Python 3 handles Unicode by default. Always use the u flag in JS when dealing with international text or emoji.

y - Sticky Flag (JS Only)

syntax
JS: /pattern/y
example
JS:  const re = /\d+/y;
     re.lastIndex = 4;
     re.exec('abc 123 xyz')   // ["123"] (starts exactly at index 4)
     re.exec('abc 123 xyz')   // null (index 7 is a space, not a digit)
output
First exec: "123" at index 4
Second exec: null (must match exactly at lastIndex)

Note The y flag forces the match to start at lastIndex, unlike g which searches forward from lastIndex. Useful for building tokenizers/lexers where you need sequential, contiguous matching. No Python equivalent -- Python's re.match already anchors to the start.

x - Verbose / Free-Spacing (Python)

syntax
Python: re.VERBOSE or re.X
example
Py:  pattern = re.compile(r'''
       \d{4}    # year
       -        # separator
       \d{2}    # month
       -        # separator
       \d{2}    # day
     ''', re.VERBOSE)
     pattern.search('2026-04-04')
output
Matches "2026-04-04"

Note Allows whitespace and comments in the pattern for readability. Spaces and # comments are ignored unless escaped or inside a character class. No native JS equivalent, but you can concatenate strings or use template literals to achieve similar readability.

String Operations with Regex

JS: test() - Boolean Check

syntax
/pattern/.test(string)  =>  boolean
example
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test('user@site.com');
console.log(isEmail);
output
true

Note Returns true/false. The fastest way to check if a pattern matches. Warning: when using test() with the g flag, the regex remembers lastIndex between calls, which can cause alternating true/false results. Avoid g with test() or reset lastIndex = 0.

JS: match() - Find Matches

syntax
string.match(/pattern/)  or  string.match(/pattern/g)
example
// Without g: returns first match + groups
'Price: $42.99'.match(/(\$)(\d+\.\d{2})/)
// With g: returns all matches (no groups)
'$42.99 and $18.50'.match(/\$\d+\.\d{2}/g)
output
Without g: ["$42.99", "$", "42.99"]
With g: ["$42.99", "$18.50"]

Note Behavior changes completely depending on the g flag. Without g, you get detailed info (groups, index). With g, you get a flat array of all matches but lose group details. Use matchAll for both multiple matches AND group info.

JS: matchAll() - Iterate All Matches

syntax
string.matchAll(/pattern/g)  =>  iterator of match objects
example
const text = 'dates: 2026-04-04, 2025-12-25';
for (const m of text.matchAll(/(\d{4})-(\d{2})-(\d{2})/g)) {
  console.log(m[0], '=> year:', m[1]);
}
output
"2026-04-04 => year: 2026"
"2025-12-25 => year: 2025"

Note Returns an iterator, so use for...of or [...spread]. Requires the g flag or it throws an error. This is the modern replacement for the while(exec()) loop pattern.

JS: replace() / replaceAll()

syntax
string.replace(/pattern/, replacement)
string.replaceAll(/pattern/g, replacement)
example
// Backreference in replacement
'John Smith'.replace(/(\w+) (\w+)/, '$2, $1')
// Function replacement
'hello'.replace(/\w/g, c => c.toUpperCase())
output
"Smith, John"
"HELLO"

Note In the replacement string, $1 $2 refer to captured groups, $& is the full match, $` is before-match, $' is after-match. Use a function for dynamic replacements. replaceAll() requires the g flag on regex arguments.

JS: split() with Regex

syntax
string.split(/pattern/)
example
'one, two;  three|four'.split(/[,;|]\s*/)
output
["one", "two", "three", "four"]

Note Split accepts a regex as the separator. If the regex contains capturing groups, the captured text is included in the result array. This can be surprising -- use non-capturing groups (?:) if you don't want separator parts in the output.

JS: exec() - Detailed Match

syntax
regex.exec(string)  =>  match object or null
example
const re = /(?<word>\w+)/g;
let m;
while ((m = re.exec('hello world')) !== null) {
  console.log(`Found "${m[0]}" at index ${m.index}`);
}
output
"Found \"hello\" at index 0"
"Found \"world\" at index 6"

Note Returns one match at a time with full detail (groups, index, named groups). With the g flag, successive calls advance through the string via lastIndex. Modern code should prefer matchAll() over the exec() while loop.

Python: re.search() - First Match

syntax
re.search(pattern, string)  =>  Match object or None
example
import re
m = re.search(r'(\d+)\s*(kg|lb)', 'Weight: 75 kg')
if m:
    print(m.group(0), m.group(1), m.group(2))
output
"75 kg" "75" "kg"

Note Scans the entire string and returns the first match. Use re.match() if you only want to check the beginning of the string. Always check for None before calling .group() to avoid AttributeError.

Python: re.findall() - All Matches

syntax
re.findall(pattern, string)  =>  list of strings or tuples
example
import re
re.findall(r'\b[A-Z][a-z]+\b', 'Alice met Bob in Paris')
output
["Alice", "Bob", "Paris"]

Note Returns a list of all non-overlapping matches. If the pattern has groups, findall returns a list of groups (or tuples for multiple groups) instead of the full match. Use (?:) for groups you don't want captured in the result.

Python: re.sub() - Replace

syntax
re.sub(pattern, replacement, string, count=0)
example
import re
# Backreference replacement
re.sub(r'(\w+) (\w+)', r'\2, \1', 'John Smith')
# Function replacement
re.sub(r'\d+', lambda m: str(int(m.group()) * 2), 'a3 b5')
output
"Smith, John"
"a6 b10"

Note In the replacement, \1 or \g<1> references group 1, \g<name> references named groups. Pass a function as replacement for dynamic logic. Use count=1 to replace only the first occurrence (similar to omitting the g flag in JS).

Python: re.split() - Split by Pattern

syntax
re.split(pattern, string, maxsplit=0)
example
import re
re.split(r'[,;]\s*', 'apples, oranges; bananas,  grapes')
output
["apples", "oranges", "bananas", "grapes"]

Note Like JS split, captured groups are included in the result. Empty matches at the start or end of the string produce empty strings in the output list. Use maxsplit to limit the number of splits.

Advanced Techniques

Atomic Groups (Concept)

syntax
(?>pattern)   (PCRE/Java; not native in JS or Python re)
example
PCRE:  (?>\d+)\s matches '123 ' but NOT by backtracking into digits
Python (regex module):
  import regex
  regex.search(r'(?>\d+):', '123:')
output
Matches '123:' -- once the digits are consumed, the engine cannot give them back

Note Atomic groups discard all backtracking positions once the group matches. They are a performance optimization to prevent catastrophic backtracking. In Python, use the third-party 'regex' module. In JS, there is no direct equivalent -- simulate with a possessive quantifier mindset or restructure the pattern.

Conditional Patterns

syntax
(?(condition)yes-pattern|no-pattern)   (Python, PCRE)
example
Py (regex module):
  import regex
  # Match optionally quoted word -- closing quote only if opening exists
  regex.findall(r'(")?(\w+)(?(1)")', '"hello" world "test"')
output
Matches '"hello"' and '"test"' as quoted, 'world' as unquoted

Note Conditional patterns branch based on whether a group participated in the match. Python's built-in re supports (?(id)yes|no) syntax. Not available in JavaScript. Useful for matching balanced optional delimiters.

Recursive Patterns

syntax
(?R) or (?P>name)   (PCRE, Python regex module)
example
Python (regex module):
  import regex
  pattern = r'\((?:[^()]*|(?R))*\)'
  regex.findall(pattern, 'a(b(c)d)e(f)g')
output
["(b(c)d)", "(f)"]  (matches balanced/nested parentheses)

Note Recursive patterns can match nested structures like balanced parentheses or nested HTML. Not available in JavaScript or Python's built-in re -- requires the third-party regex module for Python or PCRE-based engines. Extremely powerful but can be hard to debug.

Unicode Property Escapes \p{...}

syntax
JS: /\p{Letter}/u    Python: regex.findall(r'\p{L}+', text)
example
JS:  'cafe\u0301 2024 Tokyo'.match(/\p{Letter}+/gu)
Py (regex module):
  import regex
  regex.findall(r'\p{N}+', 'Price: 42 or \u0664\u0662')
output
JS: ["cafe\u0301", "Tokyo"]
Py: ["42", "\u0664\u0662"]  (\p{N} matches any Unicode number)

Note Common categories: \p{L} (letter), \p{N} (number), \p{P} (punctuation), \p{S} (symbol), \p{Z} (separator), \p{Emoji}. In JS, requires the u flag. Python's built-in re does not support \p{} -- use the regex module. Essential for internationalized text processing.

Named Group References in Replacement

syntax
JS: $<name>    Python: \g<name>
example
JS:  'Smith, John'.replace(/(?<last>\w+), (?<first>\w+)/, '$<first> $<last>')
Py:  re.sub(r'(?P<last>\w+), (?P<first>\w+)', r'\g<first> \g<last>', 'Smith, John')
output
"John Smith"

Note Named group references in replacements improve readability over numeric \1, $1 references. JS uses $<name> in the replacement string while Python uses \g<name>. Works with both replace() and re.sub().

Non-Greedy Scanning Patterns

syntax
.*?target  (scan forward minimally until target is found)
example
JS:  'START data1 END noise START data2 END'.match(/START(.*?)END/g)
Py:  re.findall(r'START(.*?)END', 'START data1 END noise START data2 END')
output
JS: ["START data1 END", "START data2 END"]
Py: [" data1 ", " data2 "]  (findall returns groups)

Note The .*? idiom scans forward character by character until the following pattern matches. This is the standard approach for extracting content between delimiters. Be aware that .*? can still cause slowdowns on long strings with no match -- consider using negated character classes instead when possible.

Compiling Regex for Reuse

syntax
JS: const re = new RegExp(pattern, flags)
Python: re.compile(pattern, flags)
example
JS:  const dateRe = new RegExp('\\d{4}-\\d{2}-\\d{2}', 'g');
     dateRe.test('2026-04-04');
Py:  date_re = re.compile(r'\d{4}-\d{2}-\d{2}')
     date_re.findall('2026-04-04 and 2025-12-25')
output
JS: true
Py: ['2026-04-04', '2025-12-25']

Note Compiling is beneficial when the same pattern is used repeatedly (e.g., in a loop). Python caches the most recent patterns automatically, but explicit compilation is clearer and avoids cache eviction. In JS, the RegExp constructor requires double-escaping backslashes in string form.

Common Mistakes

Catastrophic Backtracking

syntax
Dangerous: (a+)+$  or  (.*a){10}  or  (x+x+)+y
example
JS:  // DANGER: This can freeze your browser/server!
     // /(a+)+$/.test('aaaaaaaaaaaaaaaaaaaaaaaaaab')
     // The engine tries exponential combinations before failing
Py:  # Same risk: re.search(r'(a+)+$', 'aaa...ab')
output
Extremely slow or hangs -- exponential backtracking

Note Catastrophic backtracking occurs when the regex engine explores an exponential number of paths. It happens with nested quantifiers applied to overlapping patterns. Prevention: avoid nested quantifiers on similar character sets, use atomic groups or possessive quantifiers, add anchors, or restructure the pattern. This is one of the most common causes of regex-based DoS (ReDoS) vulnerabilities.

Greedy vs Lazy Confusion

syntax
.* vs .*?
example
Input: '<b>one</b><b>two</b>'

JS:  '<b>one</b><b>two</b>'.match(/<b>.*<\/b>/)
     // Greedy: "<b>one</b><b>two</b>"  (too much!)
JS:  '<b>one</b><b>two</b>'.match(/<b>.*?<\/b>/)
     // Lazy: "<b>one</b>"  (correct!)
output
Greedy grabs everything, lazy stops at first delimiter

Note Developers often default to .* when .*? or a negated character class [^<]* would be correct. Rule of thumb: if you want to match UP TO a delimiter, either use lazy quantifiers or a negated class that excludes the delimiter character.

Forgetting to Escape the Dot

syntax
. matches ANY character, \. matches a literal period
example
// Wrong: matches 'a.b' but also 'axb', 'a5b', etc.
/a.b/.test('axb')     // true (unintended!)

// Correct: matches only 'a.b'
/a\.b/.test('axb')    // false
/a\.b/.test('a.b')    // true
output
Unescaped dot matches too broadly

Note This is the most frequent regex beginner mistake. Domains, IP addresses, file extensions, and version numbers all contain literal dots that must be escaped. When reviewing regex, always check that dots are escaped where literal periods are intended.

Forgetting to Escape Brackets

syntax
\[ \] for literal brackets, [...] creates a character class
example
// Wrong: trying to match '[INFO]'
/[INFO]/.test('N')    // true! It's a character class matching I, N, F, or O

// Correct:
/\[INFO\]/.test('[INFO] Server started')   // true
output
[INFO] without escaping matches any single character I, N, F, or O

Note Square brackets define character classes in regex. To match literal brackets, escape both opening and closing brackets. This mistake often appears when parsing log files with bracketed labels like [ERROR] or [WARN].

^ Inside vs Outside Character Class

syntax
^abc  (start of string)    [^abc]  (negation inside class)
example
// ^ outside brackets: start-of-string anchor
/^cat/.test('category')    // true (starts with 'cat')

// ^ inside brackets: negation
/[^cat]/.test('dog')       // true (matches 'd', 'o', 'g' -- none are c, a, or t)
output
Same symbol, completely different meanings based on context

Note The ^ character serves double duty. Outside a character class it anchors to the start of the string. As the first character inside [...], it negates the class. Anywhere else inside [...], it matches a literal caret. This dual meaning catches many beginners.

Multiline Flag Misconceptions

syntax
/m changes ^ and $ only -- NOT the dot
example
// Common mistake: thinking 'm' makes dot match newlines
'line1\nline2'.match(/line1.line2/m)   // null! Dot still skips \n

// You need the 's' flag for that:
'line1\nline2'.match(/line1.line2/s)   // matches
output
m flag: affects ^/$ only
s flag: affects dot only

Note The m (multiline) flag only changes ^ and $ to match line boundaries. It does NOT make the dot match newlines -- that is the s (dotall) flag. This is one of the most common sources of confusion, especially for developers coming from editors where 'multiline' implies dot-matches-all.

Over-Complex Patterns

syntax
Avoid: /^(?:(?:[a-zA-Z0-9._%+-]+)@(?:(?:[a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}))$/
Prefer: split into steps or use a validation library
example
// Instead of one monster regex for URL validation:
function isValidUrl(str) {
  try { new URL(str); return true; }
  catch { return false; }
}

# Python: use urllib.parse
from urllib.parse import urlparse
def is_valid_url(s):
    r = urlparse(s)
    return bool(r.scheme and r.netloc)
output
Simpler, more maintainable, and fewer edge case bugs

Note Regex is powerful but not always the right tool. When a pattern grows beyond 50-80 characters, consider breaking it into multiple smaller regex checks, using built-in parsers (URL, email, date), or using a purpose-built library. Readability and maintainability matter -- a future developer (including you) will need to understand and modify it.

Global Flag + test() Stateful Bug

syntax
/pattern/g.test()  remembers lastIndex between calls
example
JS:  const re = /a/g;
     re.test('abc')   // true (lastIndex becomes 1)
     re.test('abc')   // false! (searches from index 1, no 'a' found)
     re.test('abc')   // true (lastIndex reset to 0 after failure)
output
true, false, true -- alternating unexpectedly

Note Using a regex literal with the g flag and test() or exec() in a loop or repeated calls causes this stateful bug because lastIndex persists. Fix: omit the g flag for test(), or create a new regex each time, or manually reset re.lastIndex = 0 before each test.