When should the repl argument to re.sub be a callable instead of a replacement string?
answer
- One form is a template, one is code
- The function gets the match, not the text
- Returning the match itself changes nothing
- Called once per match, in order
- A sibling call also reports how many
basics
~20 sPass a callable whenever the replacement depends on what matched — arithmetic, a lookup, a conditional skip. re.sub calls it once per match with the Match object and inserts whatever str it returns, with no template escape processing.
solid answer
~40 sA string `repl` is a **template**: re.sub scans it for group references (`\1`, `\g<name>`, `\g<0>`) and escapes, and can only splice captured text into fixed literals. The moment the replacement has to be *computed* — increment a number you captured, look the match up in a mapping, uppercase it, or leave it alone — the template runs out and you pass a function instead. re.sub calls it once per non-overlapping match, left to right, with the Match object as its only argument, and substitutes the `str` it returns; returning a non-string raises TypeError, and returning `m.group(0)` is how you decline to change that match. The returned text is used verbatim, so no group references are expanded in it. Use `re.subn` when you also need the number of substitutions made.
code
python · 7 linesimport re
def bump(m):
return f"{m.group('gene')}_{int(m.group('n')) + 1}"
print(re.sub(r"(?P<gene>[A-Z0-9]+)_(?P<n>\d+)", bump, "BRCA_1 TP53_7"))
# BRCA_2 TP53_8go deeper
Know that re.sub takes either a replacement string with group references or a function, and that it returns a new string rather than editing the original. Be able to write a two-line callable that upper-cases each match.
Explain the callable's contract precisely: one Match argument, must return a str, invoked once per match in order, return value inserted verbatim with no escape processing. Mention re.subn and the group(0) no-op.
Show judgement about where the logic lives — pattern versus callable versus a plain str.replace — and about state: a repl that accumulates counters needs an explicit per-run lifetime, or a long batch job silently carries state between inputs.
Own the maintainability call. Decide when a growing family of substitution callables should become a declarative rewrite table with tests, and when regex rewriting of a structured format should be replaced by a real parser.
### Two shapes of the same argument `re.sub(pattern, repl, string, count=0, flags=0)` accepts either a string or a callable as `repl`, and the two are genuinely different mechanisms rather than a convenience pair. **As a string**, `repl` is a template with its own small grammar. Backslash-digit (`\1` … `\99`) inserts the text of that capturing group; `\g<name>` and `\g<1>` are the explicit forms; `\g<0>` is the whole match. Standard escapes such as `\n` are honoured, `\\` is a literal backslash, and an unknown escape of an ASCII letter is an error. Everything else is copied literally. That is all a template can do: rearrange and interleave captured text with constants. ```pycon >>> import re >>> re.sub(r"(\w+)@(\w+)", r"\2:\g<1>", "ada@lab") 'lab:ada' ``` **As a callable**, `repl` is invoked once per match, in order, with a single argument: the Match object. Its return value must be a `str` — return an int and you get `TypeError: sequence item 1: expected str instance, int found`, which is a confusing message the first time you see it, because the failure surfaces when re joins the pieces. The returned string is inserted **verbatim**: nothing in it is scanned for `\1` or any other escape. ### When you need the callable Any replacement that is a function of the match: * **Arithmetic on captured text** — shift every coordinate in a report by one, renumber a suffix. * **A lookup** — map a matched identifier through a dict, falling back to leaving it alone. * **A conditional** — some matches change, others do not. `return m.group(0)` is the idiomatic no-op; it is cheaper and clearer than trying to make the pattern exclude the cases you want to skip. * **Text that came from data** — a literal replacement built from a value you did not write is unsafe as a template, because a backslash in it is interpreted; a callable returning that value sidesteps the whole grammar. ```python import re def bump(m): return f"{m.group('gene')}_{int(m.group('n')) + 1}" re.sub(r"(?P<gene>[A-Z0-9]+)_(?P<n>\d+)", bump, "BRCA_1 TP53_7") # 'BRCA_2 TP53_8' ``` ### Call semantics worth knowing The function runs once per match, not once per input string, and it runs during the scan — so it sees matches in left-to-right order and can carry state between them. `count` limits how many matches are replaced (0 means all), so the callable is not necessarily called for every match in the text. The original string is never mutated: `str` is immutable and `re.sub` returns a new one. `re.subn` does the same work but returns `(new_string, number_of_substitutions)`, which is the honest way to assert "exactly one edit happened" instead of comparing strings afterwards. ### The stateful-callable trap Because the callable can carry state, people reach for the shortest way to give it some — a default argument: ```python def label(m, seen={}): # do not do this seen[m.group(0)] = seen.get(m.group(0), 0) + 1 return f"{m.group(0)}#{seen[m.group(0)]}" ``` Default values are evaluated **once, when the `def` executes**, and that one dict is shared by every call for the life of the process. In a nightly genome-annotation run that walks thousands of files over six hours, the counter never resets between files: file two starts at whatever number file one ended on, and the corruption is invisible until someone diffs the output. The fixes are all ordinary Python — build a fresh closure per file (`def make_labeller(): counts = {}; def label(m): ...`), use a small class with `__call__`, or pass the state explicitly with `functools.partial(label, counts=fresh_dict)`. The rule of thumb: if a `repl` callable has state, make the state's lifetime a visible, per-call decision. ### Choosing between the forms Prefer the string template when the replacement really is "these captures in a different arrangement" — it is shorter, it is obvious to a reader, and it avoids a Python call per match. Prefer the callable as soon as a conditional or a computation appears, or as soon as any part of the replacement text comes from data. And when neither a pattern nor a computation is involved at all — a fixed substring swapped for another — `str.replace` is faster and says exactly what it does. ### In an interview The answer that lands is: string repl is a template that can only reference groups; a callable receives the Match and returns the replacement, so it handles anything computed or conditional; `m.group(0)` means "leave this one". Adding the `re.subn` count and the "no escape processing on the return value" detail is what separates a used-it answer from a read-about-it answer.
- What does re.subn give you that re.sub does not?`re.subn` returns a `(new_string, count)` tuple where `count` is the number of substitutions actually made. It is the clean way to assert an expectation — exactly one edit, or at least one — instead of comparing the result against the input to infer whether anything changed. The substitution work is identical; only the return shape differs.
- How does a callable repl leave certain matches untouched?Return `m.group(0)`, the matched text itself, so the substitution is a no-op for that match. That is usually better than complicating the pattern to exclude those cases, because the decision often depends on data the regex cannot see — a lookup table, a threshold, a flag. The scan continues after the match either way.
- How would you carry counters across matches without using a mutable default argument?Close over a local dict from a factory function, use a small class with `__call__` holding the state on `self`, or bind fresh state per invocation with `functools.partial`. A mutable default is evaluated once at `def` time and shared for the whole process, so state leaks between unrelated calls — the classic silent corruption in long batch runs.
saying these in an interview costs you the question
- Thinks the callable receives the matched string, not the Match object
- Returns a Match object or None from the repl callable
- Believes re.sub mutates the original string in place
- Puts \1 in the callable's return value expecting expansion
- Says a string repl can call a function or run a conditional
- Keeps repl state in a mutable default argument