configparser ${} Syntax Silently Not Interpolated (tested)
Plenty of config systems (Docker Compose, Spring, many YAML/INI tools) use ${section:option} or ${VAR} for variable substitution. We measured what Python's default ConfigParser does with that exact syntax — and it isn't an error. It's silence.
1. The default interpolation doesn't recognize ${...} — and doesn't complain
import configparser
c = configparser.ConfigParser()
c.read_string("""
[section]
base = /home/user
path = ${section:base}/data
""")
print(c.get("section", "path"))
${section:base}/data
Measured: no exception, no warning — read_string() parses cleanly and get() returns the value with ${section:base} left exactly as written, not substituted with /home/user. The default BasicInterpolation only understands %(option)s syntax; it treats $ and { as ordinary characters, so the reference simply isn't recognized as something to expand.
2. Same silent result across sections, which is ${}'s main selling point elsewhere
c2 = configparser.ConfigParser()
c2.read_string("""
[common]
home = /home/user
[section]
path = ${common:home}/data
""")
print(c2.get("section", "path"))
${common:home}/data
Measured: cross-section references — the exact pattern ${section:option} is designed for — are just as silently un-expanded under the default interpolation. If this file came from copying an example written for ExtendedInterpolation (or ported from a non-Python config format that uses ${}), every downstream reader of path gets a literal, broken string with zero signal that anything went wrong.
3. Fix: use ExtendedInterpolation, or use the %(option)s syntax BasicInterpolation actually supports
c3 = configparser.ConfigParser(interpolation=configparser.ExtendedInterpolation())
c3.read_string("""
[common]
home = /home/user
[section]
path = ${common:home}/data
""")
print(c3.get("section", "path"))
/home/user/data
Measured: passing interpolation=configparser.ExtendedInterpolation() to the constructor makes ${section:option} (and same-section ${option}) expand correctly, including across sections. Alternatively, if you don't need ${} specifically, rewriting the value with the default syntax — %(base)s for same-section, no built-in cross-section shorthand without Extended — also works under plain BasicInterpolation. Either way, the fix is explicit and one-line; the dangerous part is that the broken version never raises anything to point you at it.
Frequently asked questions
Does configparser support ${section:option} interpolation syntax?
Only with ExtendedInterpolation. We measured the default ConfigParser (BasicInterpolation) leaving ${section:option} completely unexpanded, as a literal string, with no error.
Why didn't I get an error when my configparser interpolation didn't work?
We confirmed BasicInterpolation treats $ and { as plain characters, not interpolation syntax, since it only recognizes %(option)s. Unrecognized syntax isn't invalid syntax to it, so nothing raises — the value is just returned as-is.
How do I enable ${} style variable substitution in configparser?
We verified passing interpolation=configparser.ExtendedInterpolation() to ConfigParser() makes ${option} (same section) and ${section:option} (cross-section) expand correctly.
Does this silent failure also happen with cross-section references?
Yes — we measured ${common:home} under the default interpolation returning the literal unexpanded string even though cross-section lookup is exactly what ${section:option} syntax is meant for.
Is %(option)s the same thing as ${option} in configparser?
No. %(option)s is BasicInterpolation's syntax and works by default; ${option} and ${section:option} are ExtendedInterpolation's syntax and require explicitly passing interpolation=ExtendedInterpolation() to ConfigParser().
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in configparser module: parsed a value using ${section:option} syntax under the default ConfigParser (BasicInterpolation) and under ConfigParser(interpolation=ExtendedInterpolation()), comparing the returned string in both cases. PlainHeaders is our extension; Python's configparser module is built in.