string.Template: $name_backup Greedily Eats Trailing Text (tested)

string.Template looks like simple find-and-replace: put $name in a string, pass name='value', done. We measured what happens when a placeholder is immediately followed by more letters, digits, or an underscore, and the result silently breaks a common pattern: building filenames and config keys.

1. $name_backup is parsed as one placeholder, not $name plus literal text

from string import Template

t = Template("$name_backup.txt")
print(t.safe_substitute(name="report"))
$name_backup.txt

Measured: nothing gets substituted at all. Template identifiers follow Python identifier rules -- letters, digits, and underscores -- and matching is greedy: it reads as far as it can into a valid identifier before stopping. So $name_backup is parsed as one single placeholder named name_backup, not $name followed by the literal text _backup. Since no value named name_backup was supplied, safe_substitute() just leaves the whole $name_backup.txt untouched in the output -- no error, no warning, wrong filename.

2. substitute() confirms it: the KeyError names the wrong, merged identifier

from string import Template

t = Template("$name_backup.txt")
print(t.substitute(name="report"))
Traceback (most recent call last):
  ...
KeyError: 'name_backup'

Measured: substitute() (the strict version, which raises instead of leaving placeholders untouched) confirms exactly what happened -- it's missing a key called name_backup, not name. If you supplied name="report" and expected that to be enough, this error is genuinely confusing: the key you passed isn't the one it's complaining about.

3. The fix: wrap the placeholder in braces to mark exactly where it ends

from string import Template

t = Template("${name}_backup.txt")
print(t.safe_substitute(name="report"))
report_backup.txt

Measured: ${name} explicitly delimits the placeholder, so the following _backup.txt is treated as literal text again. Use the brace form any time a placeholder is directly followed by a letter, digit, or underscore -- which is most of the time in filenames, SQL-ish identifiers, and config keys. A trailing space, dot right after a non-identifier character, or punctuation like - doesn't need braces, since those characters already stop identifier matching on their own; it's specifically underscores and alphanumerics that silently get absorbed.

Frequently asked questions

Why doesn't string.Template substitute $name_backup when I pass name='value'?

We measured this: Template parses $name_backup as one identifier, name_backup, because underscores are valid identifier characters and matching is greedy. There's no placeholder called just 'name' in that template -- it needs the value under the key 'name_backup', or braces to separate it.

Why does string.Template.substitute() raise KeyError for a key I never typed?

We confirmed this: if your template has $name_backup and you only pass name=..., the KeyError names 'name_backup', not 'name', because that's the actual identifier Template parsed out of the string. The key in the error is correct -- it's just not the key you intended.

How do I stop string.Template from merging a placeholder with the text after it?

Wrap the placeholder in braces: ${name}_backup.txt instead of $name_backup.txt. We measured that this correctly substitutes just the name part and leaves _backup.txt as literal text.

Does safe_substitute() warn me when a placeholder is left unsubstituted?

No -- we measured this directly: safe_substitute() silently leaves the untouched $placeholder text (dollar sign and all) in the output with no error or warning. Use substitute() instead during development if you want missing keys to raise immediately.

When do I need braces around a string.Template placeholder?

Whenever the placeholder is immediately followed by a letter, digit, or underscore, since those characters extend the identifier Template tries to match. Punctuation like a space, dot, or hyphen right after the name already stops the match, so braces aren't required there.

Written with AI assistance. Measured on 2026-10-04 with Python 3.13.5's built-in string module: built a string.Template with a placeholder immediately followed by an underscore and more text, substituted it two ways (with and without disambiguating braces), and compared substitute() against safe_substitute() on the resulting KeyError. PlainHeaders is our extension; Python's string module is built in.