Regular Expressions Cheatsheet
Lookahead and Lookbehind
Use this Regular Expressions reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
The four forms
Lookaround is zero-width. It tests what surrounds the current position and consumes nothing, so the assertion never lands in the match text.
| Form | Name | Asserts |
|---|---|---|
(?=...) | positive lookahead | what follows matches |
(?!...) | negative lookahead | what follows does not match |
(?<=...) | positive lookbehind | what precedes matches |
(?<!...) | negative lookbehind | what precedes does not match |
import re re.findall(r'\d+(?= USD)', '10 USD 20 EUR 30 USD') # ['10', '30'] re.findall(r'\d+(?! USD)', '10 USD 20 EUR') # ['1', '20'] re.findall(r'(?<=\$)\d+', 'cost $42 and 7') # ['42'] re.findall(r'(?<!\$)\b\d+\b', 'cost $42 and 7') # ['7']
The second line is the classic surprise. \d+(?! USD) matched 1 because after consuming just 1 the next characters are 0 USD, which is not USD. Negative lookahead needs an anchor or boundary next to it: \d+\b(?! USD).
Match not followed by something
foo(?!bar) foo not followed by bar \bfoo\b(?!\s*\() foo that is not a function call ^(?!#)\S+ a line that does not start with # ^(?!.*DEBUG).*$ a whole line containing no DEBUG \.(?!\d) a period that is not a decimal point
# every line that does not contain "healthz" grep -P '^(?!.*healthz)' access.log # or, clearer and portable grep -v healthz access.log
Use grep -v for a whole-line exclusion. Reach for (?!...) when the negation is only part of a larger pattern, such as "a line with a status code that is not 2xx".
Password validation, worked
Stack independent lookaheads at the start. Each one tests the whole remaining string from position 0 and then hands the position back untouched, so they compose freely and their order does not matter.
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9])\S{10,72}$
| Piece | Requires |
|---|---|
^ | start of string |
(?=.*[a-z]) | at least one lowercase letter |
(?=.*[A-Z]) | at least one uppercase letter |
(?=.*\d) | at least one digit |
(?=.*[^A-Za-z0-9]) | at least one symbol |
\S{10,72} | 10 to 72 non-space characters, this is the part that matches |
$ | end of string |
pw = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)' r'(?=.*[^A-Za-z0-9])\S{10,72}$') bool(pw.match('Passw0rd!x')) # True bool(pw.match('password!x1')) # False, no uppercase bool(pw.match('Pw0rd!xx')) # False, too short
In real code prefer separate checks per rule, because then you can tell the user which rule failed instead of only that the password is invalid.
Lookaround for insert-only substitutions
Because lookaround consumes nothing, a replacement can insert at a position without rewriting the characters around it.
// thousands separators, right to left "1234567".replace(/\B(?=(\d{3})+$)/g, ","); // 1,234,567
# trim both ends without touching the middle re.sub(r'^\s+|\s+$', '', ' hi ') # 'hi' # comment out lines that are not already commented re.sub(r'^(?!#)', '# ', text, flags=re.M)
PCRE has a shortcut for the same idea. \K drops everything matched so far out of the reported match, so it acts as a variable-length lookbehind.
echo "key: value" | grep -Po 'key: \K.*' # value
Lookbehind width limits per flavor
This is the one portability wall people hit.
| Flavor | Lookbehind |
|---|---|
| JavaScript | unbounded, (?<=ab*) is fine |
| .NET | unbounded |
Python re | fixed width only, even (?<=a|bb) is a compile error |
Python regex package | unbounded |
| PCRE2 | fixed width, or alternatives of differing fixed widths, or {n,m}. No + or *. Use \K instead |
| Java | bounded, {1,3} fine, and it accepts + and * in practice |
| Ruby | fixed width, or fixed-width alternatives |
| Go RE2 | no lookaround at all, in either direction |
re.compile(r'(?<=ab)c') # ok re.compile(r'(?<=ab+)c') # error: look-behind requires fixed-width pattern re.compile(r'(?<=a|bb)c') # error, the branches differ in width
Workaround in Python: capture the prefix instead of asserting it, then read group 1. Workaround in Go: match the surrounding characters for real and take the capture group.
// Go RE2 has no (?<=\$), so match the $ and capture the number regexp.MustCompile(`\$(\d+)`).FindStringSubmatch("cost $42")[1] // "42"
Cost and gotchas
- Lookaround does not backtrack into itself. Once an assertion succeeds the engine forgets how, so you cannot reference a group captured inside a lookahead from a later part of the pattern in every flavor (JavaScript and Python do keep it, .NET does too).
- A group inside a negative lookaround never captures usefully, since the assertion only succeeds when its body failed.
(?=.*x)scans to the end of the string each time. Four stacked lookaheads on a long input is four full scans. Fine for a password field, wasteful in a loop over megabytes.^(?!.*X).*$over a huge line is O(n) per starting position. Prefer a whole-line negation in the host language.\bplus a negative lookahead is almost always what you meant.\d+(?!%)is a trap,\d+\b(?!%)is not.