Unescaped input in a pattern is a correctness bug at best and a denial-of-service hole at worst.
Patterns often come from data: a search box, a list of banned words, configurable rules. Three things go wrong.
Escaping. User text like "1+1" or "C++" contains metacharacters. re.escape(s) backslash-escapes everything special, so the text is matched literally. Never interpolate raw input into a pattern; otherwise a user can type (a+)+$ and trigger catastrophic backtracking (regex injection).
Alternation order. When joining many words with |, the engine takes the first alternative that matches, not the longest. Sort by length descending (sorted(words, key=len, reverse=True)) so "new york" wins over "new". Wrap the whole alternation in a group with boundaries: rf"\b(?:{alts})\b".
Scale. An alternation of 10,000 words is compiled into a branch the engine tries one by one at every position. For big vocabularies, it's faster to tokenize the text with a simple pattern (\w+) and look each token up in a set, or to use a dedicated algorithm (Aho-Corasick).
In replacement strings, escape backslashes too (repl.replace("\\", "\\\\")), or pass a function that returns the literal text.
If users really need to supply patterns, cap their length, compile them with a try/except for re.error, and run matches against bounded input, ideally in a linear-time engine.
Example
import re
query = "C++ (beta)"
print(re.escape(query)) # C\+\+\ \(beta\)
print(bool(re.search(re.escape(query), "Using C++ (beta) today"))) # True
cities = ["new", "new york", "york"]
naive = re.compile(r"\b(?:" + "|".join(map(re.escape, cities)) + r")\b")
print(naive.findall("I love new york")) # ['new', 'york']
ordered = sorted(cities, key=len, reverse=True)
better = re.compile(r"\b(?:" + "|".join(map(re.escape, ordered)) + r")\b")
print(better.findall("I love new york")) # ['new york']
banned = {"spam", "scam", "fraud"}
words = re.findall(r"\w+", "This is not a SCAM, honest".lower())
print([w for w in words if w in banned]) # ['scam']
try:
re.compile("(unclosed")
except re.error as e:
print(e.msg) # missing ), unterminated subpatternRemember
- Always
re.escapeuser text inserted into a pattern - Sort alternatives longest first; the first match wins, not the longest
- For large word lists, tokenize and use a set instead of one huge alternation
Watch out for
- Building
re.compile(user_input)directly, letting users inject slow or invalid patterns.