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.