Substituting and Splitting

Regular Expressions in Practice

Chapter 4 ยท Substituting and Splitting

So far patterns have only found text. This chapter uses them to change it. re.sub replaces every match with something else, and re.split cuts a string wherever a pattern matches. Both are far more flexible than the plain string methods str.replace and str.split.

What was run
As in the earlier chapters, every snippet was executed and the comments show what Python printed.

re.sub

The call is re.sub(pattern, replacement, string, count=0, flags=0). It returns a new string; the original is never changed. A count of 0 means "replace all".

import re print(re.sub(r"\s+", " ", "a b \t\n c")) # a b c print(re.sub(r"\d", "#", "tel 555-0199")) # tel ###-#### print(re.sub(r"\d", "#", "a1b2c3d4", count=2)) # a#b#c3d4 print(re.subn(r"\d", "#", "a1b2c3")) # ('a#b#c#', 3)

re.subn works the same way but returns a tuple of the new string and the number of replacements, which is useful when you need to know whether anything changed.

Backreferences in the replacement

The replacement string can reuse what the pattern captured. Write \1, \2 for numbered groups, or \g<name> for named ones. This is how you reorder text.

print(re.sub(r"(\w+), (\w+)", r"\2 \1", "Smith, Ann")) # Ann Smith pat = r"(?P<y>\d{4})-(?P<m>\d\d)-(?P<d>\d\d)" print(re.sub(pat, r"\g<d>/\g<m>/\g<y>", "on 2026-11-02 and 2025-01-09")) # on 02/11/2026 and 09/01/2025 print(re.sub(r"(\d+)", r"\g<1>0", "7 and 12")) # 70 and 120

The last line shows why \g<1> exists: \10 would be read as group ten, not group one followed by a zero. Use the \g<...> form whenever a digit follows the reference.

Backreferences inside the pattern

A group can also be referred to within the pattern itself, to demand a repeat of whatever it matched.

print(re.sub(r"(\w)\1", r"<\1\1>", "book keeper tree")) # b<oo>k k<ee>per tr<ee>

Here (\w)\1 matches a doubled letter. In the pattern, \1 means "the same character group 1 just matched"; in the replacement, it means "insert group 1".

A function as the replacement

When plain reordering is not enough, pass a function. re.sub calls it once per match with the match object and substitutes whatever string it returns. The replacement function can do arithmetic, look things up, or format.

print(re.sub(r"(\d+)", lambda m: str(int(m.group()) * 2), "3 apples and 10 pears")) # 6 apples and 20 pears print(re.sub(r"\b[a-z]", lambda m: m.group().upper(), "the quick brown fox")) # The Quick Brown Fox def money(m): return f"{int(m['n']):,}" print(re.sub(r"(?P<n>\d{4,})", money, "pop 1234567 and 9876")) # pop 1,234,567 and 9,876

The function must return a string, which is why the first example wraps the number in str(). Inside the function, m.group(), m[1] and m['n'] all work as in earlier chapters.

Escapes in the replacement string

The replacement string is itself interpreted: a backslash starts an escape. A literal backslash in the output must be doubled. When literal text containing special characters is used as the pattern, protect it with re.escape.

print(re.sub(r"\\", "/", r"C:\dir\file")) # C:/dir/file print(re.sub("a", r"\\", "cat")) # c\t print(repr(re.sub("a", r"\n", "cat"))) # 'c\nt' (a real newline) print(re.escape("1+1=2?")) # 1\+1=2\? print(re.sub(re.escape("1+1"), "two", "1+1=2")) # two=2

A function replacement avoids the escape rules altogether, because its return value is used as-is. If a substitution inserts user-supplied text, return it from a function.

Empty matches

A pattern that can match nothing replaces at every position between characters, because an empty match exists everywhere.

print(re.sub(r"x*", "-", "abc")) # -a-b-c-

re.split

re.split(pattern, string, maxsplit=0, flags=0) returns a list of the pieces between matches. Unlike str.split, the separator can be a pattern.

print(re.split(r",", "a,b,,c")) # ['a', 'b', '', 'c'] print(re.split(r"\s*,\s*", "a , b,c , d")) # ['a', 'b', 'c', 'd'] print(re.split(r"[;,]\s*", "a; b,c", maxsplit=1)) # ['a', 'b,c'] print(re.split(r"\W+", "Hello, world! It's me.")) # ['Hello', 'world', 'It', 's', 'me', '']

Two things to notice. Consecutive separators produce an empty string between them ('a,b,,c'). And a separator at the very end leaves an empty string after it, as in the last line; filter these out with [p for p in parts if p] when you do not want them.

Keeping the separators

If the split pattern contains a capturing group, the text it matched is included in the result between the pieces.

print(re.split(r",", "a,b,c")) # ['a', 'b', 'c'] print(re.split(r"(,)", "a,b,c")) # ['a', ',', 'b', ',', 'c'] print(re.split(r"\d", "a1b2c")) # ['a', 'b', 'c'] print(re.split(r"\d", "1a2b3")) # ['', 'a', 'b', '']

Splitting at a position

Lookaround (covered in the next chapter) can split without consuming anything. As a preview, this cuts after sentence-ending punctuation but keeps it attached. The second example splits on blank lines.

print(re.split(r"(?<=[.!?]) +", "Hi. How are you? Fine!")) # ['Hi.', 'How are you?', 'Fine!'] print(re.split(r"\n\s*\n", "p1 line\nstill p1\n\np2")) # ['p1 line\nstill p1', 'p2']
NeedPlain string methodRegex
Fixed text replaceds.replace("a", "b")Not needed
Varying text replacednot possiblere.sub(pattern, repl, s)
Split on one fixed separators.split(",")Not needed
Split on any run of spacess.split()Not needed
Split on several or variable separatorsnot possiblere.split(pattern, s)
Choosing the tool
If the text is fixed and the separator is a single fixed string, the plain string method is simpler and faster. Reach for re.sub and re.split when the thing you are replacing or splitting on varies.

Challenges

Exercise 1

Given text = "Smith, Ann: 2026-11-02\nJones, Bo: 2025-01-09", use one re.sub call with named groups and re.M to turn each line into First Last: dd/mm/yyyy. Then use re.subn to count how many lines have a Last, First name at the start.

๐Ÿ“„ View solution
Exercise 2

Use a replacement function to convert every Celsius reading in "Mon 20C, Tue -5C, Wed 21.5C, 3Cats" to Fahrenheit with one decimal place (for example 68.0F). Negative and decimal values must work, and 3Cats must be left alone. Say how your pattern protects it.

๐Ÿ“„ View solution
Exercise 3

Split "a1+b2*(c-3)" into tokens while keeping the operators and brackets, and filter out the empty strings. Then split "Para one\nline two\n\n\nPara two\n\nPara three" into paragraphs on one or more blank lines, and show the effect of maxsplit=1.

๐Ÿ“„ View solution

Quick Reference

  • re.sub(p, r, s, count=0): returns a new string; re.subn also returns the count
  • In the replacement: \1, \g<1>, \g<name> insert groups
  • In the pattern: \1 demands a repeat of group 1
  • Function replacement: receives the match, must return a string
  • Literal text as a pattern: re.escape(text)
  • re.split(p, s, maxsplit=0): a capturing group keeps the separators
  • Empty strings: appear between adjacent separators and at the ends; filter if unwanted
Coming next
Regular Expressions in Practice 5 covers lookaround: lookahead and lookbehind, which test the surroundings of a match without consuming them.