Python re.sub: Replace Text with Regular Expressions
python **sub**: Learn how to use Python's re.sub() for regex-based text replacement, including replacement functions, backreferences, count, flags, and common pitfalls.
Python's re.sub() — the function most developers mean by python **sub** — is the standard tool for regex-based text replacement. When a plain str.replace() cannot express the pattern you need, re.sub() takes over. This article walks through its signature, its parameters, and the edge cases that trip up working developers.
The Signature of re.sub()
The function lives in the standard re module and has the following signature:
import re re.sub(pattern, repl, string, count=0, flags=0)
pattern is the regular expression to search for, repl is either a replacement string or a callable, and string is the input text. The function returns a new string with all non-overlapping matches replaced; the original string is never modified. The count parameter caps how many substitutions happen, and flags passes options such as re.IGNORECASE or re.MULTILINE to the pattern compilation.
The return value is always a str. If no match is found, re.sub() returns the input string unchanged.
Basic Replacement with a String
The simplest call passes a literal replacement string. Every match of the pattern is replaced with that text:
import re text = "order 42, order 43, order 44" result = re.sub(r"order \d+", "item", text) print(result) # item, item, item
The pattern order \d+ matches the literal word "order" followed by a space and one or more digits. Each match is replaced by the fixed string "item". Note the raw string prefix r on the pattern; without it, \d would be interpreted by Python's own string escaping rules before the regex engine ever sees it.
Using a Replacement Function
When the replacement needs to depend on the matched text, pass a callable instead of a string. The callable receives a re.Match object and must return the replacement string:
import re def expand(match): number = int(match.group(1)) return f"item-{number * 10}" text = "order 4, order 7" result = re.sub(r"order (\d+)", expand, text) print(result) # item-40, item-70
The function is invoked once per match. This is the cleanest way to transform matched content, because the logic lives in ordinary Python code rather than in regex replacement syntax. It also avoids the escaping problems that affect string replacements containing backslashes.
Backreferences and Named Groups
A replacement string can reference captured groups. The syntax \1 refers to the first group, and \g<name> refers to a named group:
import re text = "2024-03-15" result = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", text) print(result) # 15/03/2024
The replacement string is itself a raw string, which matters because \1 would otherwise be an invalid escape sequence in a normal string literal. For named groups, use \g<name> to avoid ambiguity when a group number is followed by a digit:
import re text = "price 12" result = re.sub(r"(?P<amount>\d+)", r"\g<amount>0", text) print(result) # price 120
Without the \g<...> form, \1 followed by a literal 0 would be read as group 10 or as group 1 followed by a zero, depending on the pattern. The named form removes that ambiguity.
Limiting Substitutions with count
By default, re.sub() replaces every non-overlapping match. The count parameter limits the number of replacements, applied left to right:
import re text = "a1 b2 c3 d4" result = re.sub(r"\d", "#", text, count=2) print(result) # a# b# c3 d4
Only the first two digits are replaced. This is useful when you want to sanitize a prefix of a string, such as masking the first few characters of a credit card number or log line, while leaving the rest untouched. Note that count=0 means unlimited, not zero replacements.
Flags That Change Matching Behavior
The flags parameter controls how the pattern is compiled. Common flags include re.IGNORECASE, re.MULTILINE, and re.DOTALL:
import re text = "Cat and cat and CAT" result = re.sub(r"cat", "dog", text, flags=re.IGNORECASE) print(result) # dog and dog and dog
re.MULTILINE changes the meaning of ^ and $ so they match at the start and end of each line rather than only at the start and end of the whole string. This matters when you process multi-line logs or configuration files line by line with a single re.sub() call.
Escaping Pitfalls in Replacement Strings
Backslashes in the replacement string are processed by the regex engine, not by Python. A replacement like r"\n" inserts a newline character, while r"\\n" inserts a literal backslash followed by n. This double-escaping is a frequent source of bugs:
import re text = "a b" result = re.sub(r" ", r"\\n", text) print(result) # a\nb
If the replacement text comes from user input or an external source, escape it with re.escape() before passing it to re.sub() so that any backslashes or group references in that text are treated literally:
import re user_text = r"\1" safe = re.escape(user_text) result = re.sub(r"x", safe, "x") print(result) # \1
Without the escape, \1 in the replacement would be interpreted as a reference to the first capture group, which does not exist in this pattern, and would raise an error or produce unexpected output depending on the Python version.
Performance: Reusing Compiled Patterns
Every call to re.sub() compiles the pattern unless it is already a compiled pattern object. When the same pattern is used repeatedly in a loop, compilation cost repeats on every iteration. Compile once with re.compile() and pass the compiled object to re.sub():
import re pattern = re.compile(r"item \d+") for line in lines: cleaned = pattern.sub("item", line)
The compiled object exposes the same sub() method, and the pattern is parsed only once. For a small number of calls the difference is negligible, but in tight loops over large text collections, reusing the compiled pattern avoids repeated parsing and internal allocation work.
re.sub vs str.replace
str.replace() is faster and simpler when the search text is a fixed literal with no wildcards. re.sub() is the right choice when the pattern varies, when you need capture groups, or when the replacement must be computed per match. A fixed substring replacement like text.replace("foo", "bar") does not need the regex engine at all, and using re.sub() there only adds parsing overhead. Choose str.replace() for static text and re.sub() for pattern-based substitution.