Regular expressions explained: classes, quantifiers and groups
A regular expression, or regex, is a small pattern that describes text: “four digits, a dash, two digits”, or “a word that starts with a capital letter”. Programs use them to search, to check input and to pull pieces out of a line. This guide explains the parts a pattern is made of, with examples you can paste into the regex tester, then the traps: patterns that match more than intended, patterns that can take forever, and the differences between the regex engines of JavaScript, Python and PHP. The examples use JavaScript syntax, the one the tester and every browser run.
Characters that mean something else
Most characters in a pattern match themselves: cat finds the letters c, a and t in that order. Fourteen do not, because they have a job in the pattern: . ^ $ * + ? ( ) [ ] { } | \ and, inside JavaScript code, the slash that ends the pattern. To match one of them literally, put a backslash before it.
1.5Matches 1.5, but also 1x5 and 1-5: the dot stands for any one character.1\.5Matches only 1.5: the backslash makes the dot a dot.\$\d+Matches $30: an escaped dollar sign, then digits.
Character classes
A class matches one character from a set. Square brackets list the set yourself; a backslash and a letter give a ready-made one; a capital letter means the opposite.
| Pattern | Matches one… |
|---|---|
[aeiou] | vowel, a, e, i, o or u |
[^aeiou] | character that is not one of those, including spaces and digits |
[a-z0-9] | lower-case letter from a to z, or a digit |
\d | digit 0 to 9 (\D: anything else) |
\w | letter A to Z or a to z, digit or underscore (\W: anything else) |
\s | space, tab, line break or other whitespace (\S: anything else) |
. | character except a line break (any character with the s flag) |
\p{L} | letter in any alphabet, with the u or v flag |
In JavaScript, \d and \w only know the basic Latin alphabet: \w+ splits São into S and o, and \d does not match the Arabic-Indic digit ٣. With the u flag, \p{L}+ matches São whole and \p{Nd} matches digits in every script. Python 3 does the opposite: for ordinary text its \d and \w already cover Unicode, unless the ASCII flag is set. PHP's PCRE2 engine stays with ASCII unless Unicode properties are switched on, for example with (*UCP) at the start of the pattern.
Quantifiers: how many
A quantifier says how many times the thing before it may repeat. It applies to one character, one class or one group in brackets.
| Pattern | Means |
|---|---|
a* | zero or more |
a+ | one or more |
a? | zero or one, that is optional |
a{3} | exactly three |
a{3,} | three or more |
a{2,4} | from two to four |
Quantifiers are greedy: they take as much as they can and give back only what the rest of the pattern needs. On <b>bold</b>, the pattern <.+> matches the whole string, from the first < to the last >. Adding a question mark makes the quantifier lazy, taking as little as possible: <.+?> matches <b> and then </b>. colou?r matches both color and colour, and \d{3,} finds numbers of three digits or more.
Anchors and word boundaries
Anchors match a position, not a character. ^ is the start of the text and $ the end; with the m flag they become the start and end of each line. \b is a word boundary, the point between a word character and anything else, so \bcat\b finds cat as a word but not inside concatenate. Without anchors, a pattern used to check input also accepts text with extra characters around the part that matches: \d{4} finds four digits inside abc12345, while ^\d{4}$ accepts only exactly four digits.
\bcat\bOn “cat concatenate cat.”: two matches, the two words cat.^\d{4}$Accepts 2026; rejects 12026 and 2026a.
Groups, alternatives and back-references
Brackets group part of a pattern so a quantifier or an alternative applies to all of it, and they capture the text they matched so a program can read it: (\d{4})-(\d{2})-(\d{2}) captures year, month and day separately. (?:…) groups without capturing, and (?<year>\d{4}) gives the capture a name.
The vertical bar means “or”, and it has the lowest precedence of all: ^cat|dog$ means “cat at the start” or “dog at the end”, so it finds cat in catalog and dog in hotdog. To mean “the whole text is cat or dog”, group the alternatives: ^(?:cat|dog)$.
A back-reference matches the same text a group already captured: (\w)\1 finds a letter followed by itself, oo, kk and ee in bookkeeper, and \k<name> does the same for a named group.
Lookahead and lookbehind
A lookaround checks what comes before or after a position without including it in the match. (?=…) requires something to follow, (?!…) forbids it; (?<=…) requires something to precede, (?<!…) forbids it. They are useful to pick out a number by its unit or currency, or to stack several conditions on the same text.
\d+(?=%)On “20% of 500”: matches 20, the number followed by a percent sign, without the sign.(?<=€)\d+On “€25 and $30”: matches 25, the amount after a euro sign.^(?=.*\d).{8,}$Accepts password1 and rejects password: at least eight characters, one of them a digit.
Flags
Flags change how the whole pattern is applied. In JavaScript they follow the closing slash, as in /cat/gi; in the tester they have their own field.
| Flag | Effect |
|---|---|
g | find every match, not only the first |
i | ignore the difference between capitals and small letters |
m | ^ and $ match at each line |
s | the dot also matches line breaks |
u | Unicode mode: \p{…}, and emoji count as one character |
v | Unicode mode with set operations in classes; use it instead of u |
y | sticky: match only where the previous match ended |
d | record where each group starts and ends |
Checking emails, dates and other formats
A regex can check the shape of an input, not whether it is real. The HTML standard defines what an <input type="email"> accepts with a pattern of its own, and says openly that it is a deliberate departure from RFC 5322, the email format standard, which allows forms such as quoted names with spaces that almost no one uses. It accepts ana@example.com and also ana@localhost; it rejects ana@my_domain.example, because a domain name cannot contain an underscore. Whether the mailbox exists only a message can tell.
Dates are similar: ^\d{4}-\d{2}-\d{2}$ accepts 2026-02-31, which does not exist. Check the shape with the pattern, then let a date function decide whether the date is valid. And do not parse HTML or JSON with regular expressions: nested structures need a parser.
When a pattern takes forever
Most engines, including those of JavaScript, Python and PHP, work by backtracking: when a path fails, they go back and try another way of splitting the text between the parts of the pattern. A group that repeats something that itself repeats, such as ^(a+)+$, can be split in a number of ways that doubles with every extra character. On a line of a’s that ends in an exclamation mark the pattern can never match, but the engine tries every split before it gives up. OWASP calls this a regular-expression denial of service, ReDoS, and lists patterns like (a+)+$, ([a-zA-Z]+)*$ and (a|aa)+$.
The cure is to remove the ambiguity: ^a+$ describes the same text and fails at once. Avoid nested quantifiers and alternatives that can match the same characters, and never run a pattern supplied by a user without a time limit. The regex tester runs each search in a separate worker and stops it after half a second, so a pattern like this is reported instead of freezing the page.
Not every engine is the same
The basics above work almost everywhere, but the details change between languages. A pattern written for one engine should be tested in the other before it is relied on there.
| Feature | JavaScript | Python (re) | PHP (PCRE2) |
|---|---|---|---|
| Named group | (?<name>…) | (?P<name>…) | both forms |
| \d and \w | ASCII only | Unicode by default | ASCII unless UCP is on |
| Possessive quantifiers and atomic groups (a++, (?>…)) | no | yes, since Python 3.11 | yes |
| Inline flags such as (?i) | only the scoped form (?i:…), since ECMAScript 2025 | yes | yes |
Lookbehind also differs: JavaScript accepts one of varying length, such as (?<=[a-z]+), while Python requires a fixed width and rejects it. Python also writes the backslashes differently in code: a raw string, r"\d+", keeps them as typed, while JavaScript patterns between slashes need no doubling. In a JavaScript string passed to new RegExp(), every backslash must be written twice.
Frequently asked questions
Why does my pattern match only once?
In JavaScript a search stops at the first match unless the g flag is set. In the tester, add g to the flags.
Why does \w not match accented letters?
Because in JavaScript \w means only A to Z, a to z, digits and underscore. Use \p{L} with the u flag for letters in any alphabet.
How do I match a dot, a bracket or a question mark?
Put a backslash before it: \. \( \? and so on. Inside square brackets most of them lose their meaning, so [.?] matches a dot or a question mark.
Can a regular expression validate an email address?
Only its shape. The pattern of the HTML standard is a sensible choice for forms; whether the address receives mail can only be checked by sending a message.
Why did my pattern freeze the program?
It probably repeats a group that itself repeats, such as (a+)+, and the text almost matches. Rewrite it without nested quantifiers, and run patterns from users with a time limit.
References
- MDN Web Docs — Regular expressions (JavaScript guide)
- MDN Web Docs — Regular expression syntax cheat sheet
- ECMAScript® Language Specification — RegExp (Regular Expression) Objects
- TC39 — Finished proposals (RegExp modifiers and duplicate named groups: ECMAScript 2025)
- Python documentation — re: regular expression operations
- PCRE2 — pcre2pattern: syntax and semantics of the regular expressions
- OWASP — Regular expression Denial of Service (ReDoS)
- WHATWG HTML Standard — Valid email address
- RFC 5322 — Internet Message Format (October 2008)
Related guides
- IP addresses explained: IPv4, IPv6 and the reserved ranges
- Subnets and CIDR explained: prefixes, masks and usable hosts
- DNS records explained: A, AAAA, CNAME, MX, TXT and NS
- Is a website down, or is it just you? How to check, step by step
- HTTP status codes and Cloudflare errors: what 403, 404, 429, 500, 503 and 521 mean
- Unix timestamps and time zones explained
- Counting dates: days between dates, leap years and ISO weeks
- Time zones and daylight saving time: meetings across cities
- Image formats explained: JPEG, PNG, WebP, AVIF and HEIC
- Image size explained: pixels, DPI, file size and quality
- Phone photos explained: HEIC, EXIF metadata and your privacy
- Password strength explained: length, entropy and passphrases
- Hash functions explained: SHA-256, checksums and password hashing
- JSON Web Tokens explained: claims, signatures and safe validation
- API keys explained: length, format, storage and rotation
- UUIDs explained: v4, v7 and choosing the right version
- QR codes explained: capacity, error correction and print size
- Percentages and VAT explained: adding, removing and stacking
- Common JSON errors and how to fix them
- CSV files explained: delimiters, quotes, encoding and Excel
- Text encodings explained: UTF-8, Base64 and URL encoding
- Editing PDFs in the browser: what merging, splitting and converting keep