It's not immediately obvious how to escape the repl (replacement for matches) argument to re.sub() and re.subn() if repl is chosen by a potentially hostile actor. Obviously, re.escape() isn't the answer, as that escapes far too much.
The right answer seems to be escaped_repl = raw_repl.replace(bslash, bslash*2) where bslash = '\\'. It might be worth adding this to the documentation.
Here's the code I used to empirically validate the "right answer" given above (checked on Python 3.8 & 3.12):
Obviously, re.escape() isn't the answer, as that escapes far too much.
It was not obvious to me whether escaping "too much" is harmful, but in this case yes.
For example it escapes various whitespace re.escape(r' ') == r'\ ', which is harmless for pattern, but in repl results in the backslash staying in the result due to the rule "Other unknown escapes such as \& are left alone".
But first, what's your exact goal? Which of the repl abilities do you want to expose vs. block?
\1 group references?
\g<...> group references?
Regular escapes like in string-literals \n, \t etc. (sub() supports these so you can use raw string literal for repl)?
A way to express a literal backslash in the replacement.
Do you want verbatim replacement strings — no dynamic references, and your API receives the replacement 1:1, with single backslash meaning single backslash in the result?
In that case I think .replace('\\', '\\\\') works, with all backslashes doubled no other escape sequences can be recognized ✅.
But consider instead giving a replfunction, this passes your test too ✅:
forrawintest_cases:
defrepl(match):
returnraw# ^ fine as we call it in same loop iteration.# If you needed a long-lived closure, could `def repl(match, raw=raw):`.got, change_count=re.subn(base_regex, repl, base_input)
Don't think of passing a function is "advanced last-resort escape hatch", it's actually mentally simpler than the string repl notation! Whatever the function returns is emitted 1:1.
Activity
cben commented on Feb 8, 2026
It was not obvious to me whether escaping "too much" is harmful, but in this case yes.
For example it escapes various whitespace re.escape(r' ') == r'\ ', which is harmless for pattern, but in repl results in the backslash staying in the result due to the rule "Other unknown escapes such as \& are left alone".
But first, what's your exact goal? Which of the repl abilities do you want to expose vs. block?
Do you want verbatim replacement strings — no dynamic references, and your API receives the replacement 1:1, with single backslash meaning single backslash in the result?
In that case I think .replace('\\', '\\\\') works, with all backslashes doubled no other escape sequences can be recognized ✅.
But consider instead giving a repl function, this passes your test too ✅:
Don't think of passing a function is "advanced last-resort escape hatch", it's actually mentally simpler than the string repl notation! Whatever the function returns is emitted 1:1.
cben commented on Feb 8, 2026
Oh, I see re.escape doc already explains this since #3907 (emphasis mine):
@finite-state-machine do you feel this can be closed? If not, please suggest concrete doc improvement.
Restructure re.sub docs, clarify aspects of `repl` notation
Restructure re.sub docs, clarify aspects of `repl` notation
Restructure re.sub docs, clarify aspects of `repl` notation
Restructure re.sub docs, clarify aspects of `repl` notation
Restructure re.sub docs, clarify aspects of `repl` notation